Skip to content

Commit dc77aac

Browse files
committed
gen-lezer: fix @Specialize and @top emission (valid syntax, still LR-blocked)
Two real syntax bugs in the Lezer output: - `@specialize<Ident, keyword> { "abstract" kw_abstract, … }` is not valid Lezer — there is no block form. Emit one rule per keyword instead (`kw_abstract { @Specialize<Ident, "abstract"> }`), the inline-expression form @lezer/javascript uses. - The entry rule is named `Program`, so `@top Program { Program }` collided with the rule's own definition ("Duplicate definition of rule 'Program'"). Promote the entry rule to @top in place (prefix its definition) rather than emitting a wrapper. The grammar is now syntactically valid but still does not build: the optional-modifier soup in rules like ClassMember (716 choices) / TypeMember (402) blows up Lezer's LR table generation combinatorially — and behind that lies the `<`/type-vs-expression LR(1) shift/reduce wall. Both TS and JS hang on the choice blowup. tree-sitter's GLR absorbs all of this (derived highlighter at 95.9%); Lezer's LR(1) cannot, so there is still no derived Lezer number. Refine the README note accordingly.
1 parent 00d9cff commit dc77aac

4 files changed

Lines changed: 137 additions & 145 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ TypeScript — token-family accuracy (higher = more correct)
7777
<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>
7878
<!-- bench:end -->
7979

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 grammar is expressive enough to trip Lezer's LR(1) shift/reduce wall — it parses under tree-sitter's GLR but won't build under Lezer.)*
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.)*
8181

8282
> **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.
8383

examples/lezer/javascript.grammar

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
l0 @right
2323
}
2424

25-
@top Program { Program }
2625
DecoratorExpr {
2726
Decorator (ParenL (Expr (Comma Expr)*)? ParenR)?
2827
}
@@ -140,63 +139,61 @@ ImportClause {
140139
ImportSpecifier {
141140
Ident (kw_as Ident)?
142141
}
143-
Program {
142+
@top Program {
144143
(Decl | Stmt)*
145144
}
146145

147146
// Keyword specialization over the identifier token:
148-
@specialize<Ident, keyword> {
149-
"accessor" kw_accessor,
150-
"as" kw_as,
151-
"async" kw_async,
152-
"await" kw_await,
153-
"break" kw_break,
154-
"case" kw_case,
155-
"catch" kw_catch,
156-
"class" kw_class,
157-
"const" kw_const,
158-
"constructor" kw_constructor,
159-
"continue" kw_continue,
160-
"debugger" kw_debugger,
161-
"default" kw_default,
162-
"delete" kw_delete,
163-
"do" kw_do,
164-
"else" kw_else,
165-
"export" kw_export,
166-
"extends" kw_extends,
167-
"false" kw_false,
168-
"finally" kw_finally,
169-
"for" kw_for,
170-
"from" kw_from,
171-
"function" kw_function,
172-
"get" kw_get,
173-
"if" kw_if,
174-
"import" kw_import,
175-
"in" kw_in,
176-
"instanceof" kw_instanceof,
177-
"let" kw_let,
178-
"meta" kw_meta,
179-
"new" kw_new,
180-
"null" kw_null,
181-
"of" kw_of,
182-
"return" kw_return,
183-
"set" kw_set,
184-
"static" kw_static,
185-
"super" kw_super,
186-
"switch" kw_switch,
187-
"this" kw_this,
188-
"throw" kw_throw,
189-
"true" kw_true,
190-
"try" kw_try,
191-
"typeof" kw_typeof,
192-
"undefined" kw_undefined,
193-
"using" kw_using,
194-
"var" kw_var,
195-
"void" kw_void,
196-
"while" kw_while,
197-
"with" kw_with,
198-
"yield" kw_yield
199-
}
147+
kw_accessor { @specialize<Ident, "accessor"> }
148+
kw_as { @specialize<Ident, "as"> }
149+
kw_async { @specialize<Ident, "async"> }
150+
kw_await { @specialize<Ident, "await"> }
151+
kw_break { @specialize<Ident, "break"> }
152+
kw_case { @specialize<Ident, "case"> }
153+
kw_catch { @specialize<Ident, "catch"> }
154+
kw_class { @specialize<Ident, "class"> }
155+
kw_const { @specialize<Ident, "const"> }
156+
kw_constructor { @specialize<Ident, "constructor"> }
157+
kw_continue { @specialize<Ident, "continue"> }
158+
kw_debugger { @specialize<Ident, "debugger"> }
159+
kw_default { @specialize<Ident, "default"> }
160+
kw_delete { @specialize<Ident, "delete"> }
161+
kw_do { @specialize<Ident, "do"> }
162+
kw_else { @specialize<Ident, "else"> }
163+
kw_export { @specialize<Ident, "export"> }
164+
kw_extends { @specialize<Ident, "extends"> }
165+
kw_false { @specialize<Ident, "false"> }
166+
kw_finally { @specialize<Ident, "finally"> }
167+
kw_for { @specialize<Ident, "for"> }
168+
kw_from { @specialize<Ident, "from"> }
169+
kw_function { @specialize<Ident, "function"> }
170+
kw_get { @specialize<Ident, "get"> }
171+
kw_if { @specialize<Ident, "if"> }
172+
kw_import { @specialize<Ident, "import"> }
173+
kw_in { @specialize<Ident, "in"> }
174+
kw_instanceof { @specialize<Ident, "instanceof"> }
175+
kw_let { @specialize<Ident, "let"> }
176+
kw_meta { @specialize<Ident, "meta"> }
177+
kw_new { @specialize<Ident, "new"> }
178+
kw_null { @specialize<Ident, "null"> }
179+
kw_of { @specialize<Ident, "of"> }
180+
kw_return { @specialize<Ident, "return"> }
181+
kw_set { @specialize<Ident, "set"> }
182+
kw_static { @specialize<Ident, "static"> }
183+
kw_super { @specialize<Ident, "super"> }
184+
kw_switch { @specialize<Ident, "switch"> }
185+
kw_this { @specialize<Ident, "this"> }
186+
kw_throw { @specialize<Ident, "throw"> }
187+
kw_true { @specialize<Ident, "true"> }
188+
kw_try { @specialize<Ident, "try"> }
189+
kw_typeof { @specialize<Ident, "typeof"> }
190+
kw_undefined { @specialize<Ident, "undefined"> }
191+
kw_using { @specialize<Ident, "using"> }
192+
kw_var { @specialize<Ident, "var"> }
193+
kw_void { @specialize<Ident, "void"> }
194+
kw_while { @specialize<Ident, "while"> }
195+
kw_with { @specialize<Ident, "with"> }
196+
kw_yield { @specialize<Ident, "yield"> }
200197

201198
@skip { Shebang | JSDoc | TripleSlash | LineComment | BlockComment }
202199

examples/lezer/typescript.grammar

Lines changed: 72 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
l0 @right
2323
}
2424

25-
@top Program { Program }
2625
Type {
2726
(Ident (kw_is Type)? | Type AngleL (Type (Comma Type)*)? AngleR | Type BracketL BracketR | Type Op_7c Type | Type Op_26 Type | Op_7c Type | Op_26 Type | kw_keyof Type | kw_typeof TypeofRef | kw_readonly Type | ParenL Type ParenR | TypeParams? ParenL (Param (Comma Param)*)? ParenR Arrow Type | kw_abstract? kw_new TypeParams? ParenL (Param (Comma Param)*)? ParenR Arrow Type | BracketL (Spread? (Ident Question? Colon)? Spread? Type Question? Comma?)* BracketR | BraceL (TypeMember (Semi | Comma)?)* BraceR | kw_asserts Ident (kw_is Type)? | Type kw_extends Type Question Type Colon Type | kw_infer Ident (kw_extends Type (Question Type Colon Type)?)? | String | Number | HexNumber | OctalNumber | BinaryNumber | BigInt | Op_2d (Number | BigInt) | kw_true | kw_false | kw_null | kw_undefined | kw_void | kw_this | kw_unique kw_symbol | kw_import ParenL Type ParenR | Template | Type BracketL Type BracketR | Type Dot Ident)
2827
}
@@ -167,84 +166,82 @@ ImportClause {
167166
ImportSpecifier {
168167
Ident (kw_as Ident)?
169168
}
170-
Program {
169+
@top Program {
171170
(Decl | Stmt)*
172171
}
173172

174173
// Keyword specialization over the identifier token:
175-
@specialize<Ident, keyword> {
176-
"abstract" kw_abstract,
177-
"accessor" kw_accessor,
178-
"as" kw_as,
179-
"asserts" kw_asserts,
180-
"async" kw_async,
181-
"await" kw_await,
182-
"break" kw_break,
183-
"case" kw_case,
184-
"catch" kw_catch,
185-
"class" kw_class,
186-
"const" kw_const,
187-
"constructor" kw_constructor,
188-
"continue" kw_continue,
189-
"debugger" kw_debugger,
190-
"declare" kw_declare,
191-
"default" kw_default,
192-
"delete" kw_delete,
193-
"do" kw_do,
194-
"else" kw_else,
195-
"enum" kw_enum,
196-
"export" kw_export,
197-
"extends" kw_extends,
198-
"false" kw_false,
199-
"finally" kw_finally,
200-
"for" kw_for,
201-
"from" kw_from,
202-
"function" kw_function,
203-
"get" kw_get,
204-
"if" kw_if,
205-
"implements" kw_implements,
206-
"import" kw_import,
207-
"in" kw_in,
208-
"infer" kw_infer,
209-
"instanceof" kw_instanceof,
210-
"interface" kw_interface,
211-
"is" kw_is,
212-
"keyof" kw_keyof,
213-
"let" kw_let,
214-
"meta" kw_meta,
215-
"module" kw_module,
216-
"namespace" kw_namespace,
217-
"new" kw_new,
218-
"null" kw_null,
219-
"of" kw_of,
220-
"out" kw_out,
221-
"override" kw_override,
222-
"private" kw_private,
223-
"protected" kw_protected,
224-
"public" kw_public,
225-
"readonly" kw_readonly,
226-
"return" kw_return,
227-
"satisfies" kw_satisfies,
228-
"set" kw_set,
229-
"static" kw_static,
230-
"super" kw_super,
231-
"switch" kw_switch,
232-
"symbol" kw_symbol,
233-
"this" kw_this,
234-
"throw" kw_throw,
235-
"true" kw_true,
236-
"try" kw_try,
237-
"type" kw_type,
238-
"typeof" kw_typeof,
239-
"undefined" kw_undefined,
240-
"unique" kw_unique,
241-
"using" kw_using,
242-
"var" kw_var,
243-
"void" kw_void,
244-
"while" kw_while,
245-
"with" kw_with,
246-
"yield" kw_yield
247-
}
174+
kw_abstract { @specialize<Ident, "abstract"> }
175+
kw_accessor { @specialize<Ident, "accessor"> }
176+
kw_as { @specialize<Ident, "as"> }
177+
kw_asserts { @specialize<Ident, "asserts"> }
178+
kw_async { @specialize<Ident, "async"> }
179+
kw_await { @specialize<Ident, "await"> }
180+
kw_break { @specialize<Ident, "break"> }
181+
kw_case { @specialize<Ident, "case"> }
182+
kw_catch { @specialize<Ident, "catch"> }
183+
kw_class { @specialize<Ident, "class"> }
184+
kw_const { @specialize<Ident, "const"> }
185+
kw_constructor { @specialize<Ident, "constructor"> }
186+
kw_continue { @specialize<Ident, "continue"> }
187+
kw_debugger { @specialize<Ident, "debugger"> }
188+
kw_declare { @specialize<Ident, "declare"> }
189+
kw_default { @specialize<Ident, "default"> }
190+
kw_delete { @specialize<Ident, "delete"> }
191+
kw_do { @specialize<Ident, "do"> }
192+
kw_else { @specialize<Ident, "else"> }
193+
kw_enum { @specialize<Ident, "enum"> }
194+
kw_export { @specialize<Ident, "export"> }
195+
kw_extends { @specialize<Ident, "extends"> }
196+
kw_false { @specialize<Ident, "false"> }
197+
kw_finally { @specialize<Ident, "finally"> }
198+
kw_for { @specialize<Ident, "for"> }
199+
kw_from { @specialize<Ident, "from"> }
200+
kw_function { @specialize<Ident, "function"> }
201+
kw_get { @specialize<Ident, "get"> }
202+
kw_if { @specialize<Ident, "if"> }
203+
kw_implements { @specialize<Ident, "implements"> }
204+
kw_import { @specialize<Ident, "import"> }
205+
kw_in { @specialize<Ident, "in"> }
206+
kw_infer { @specialize<Ident, "infer"> }
207+
kw_instanceof { @specialize<Ident, "instanceof"> }
208+
kw_interface { @specialize<Ident, "interface"> }
209+
kw_is { @specialize<Ident, "is"> }
210+
kw_keyof { @specialize<Ident, "keyof"> }
211+
kw_let { @specialize<Ident, "let"> }
212+
kw_meta { @specialize<Ident, "meta"> }
213+
kw_module { @specialize<Ident, "module"> }
214+
kw_namespace { @specialize<Ident, "namespace"> }
215+
kw_new { @specialize<Ident, "new"> }
216+
kw_null { @specialize<Ident, "null"> }
217+
kw_of { @specialize<Ident, "of"> }
218+
kw_out { @specialize<Ident, "out"> }
219+
kw_override { @specialize<Ident, "override"> }
220+
kw_private { @specialize<Ident, "private"> }
221+
kw_protected { @specialize<Ident, "protected"> }
222+
kw_public { @specialize<Ident, "public"> }
223+
kw_readonly { @specialize<Ident, "readonly"> }
224+
kw_return { @specialize<Ident, "return"> }
225+
kw_satisfies { @specialize<Ident, "satisfies"> }
226+
kw_set { @specialize<Ident, "set"> }
227+
kw_static { @specialize<Ident, "static"> }
228+
kw_super { @specialize<Ident, "super"> }
229+
kw_switch { @specialize<Ident, "switch"> }
230+
kw_symbol { @specialize<Ident, "symbol"> }
231+
kw_this { @specialize<Ident, "this"> }
232+
kw_throw { @specialize<Ident, "throw"> }
233+
kw_true { @specialize<Ident, "true"> }
234+
kw_try { @specialize<Ident, "try"> }
235+
kw_type { @specialize<Ident, "type"> }
236+
kw_typeof { @specialize<Ident, "typeof"> }
237+
kw_undefined { @specialize<Ident, "undefined"> }
238+
kw_unique { @specialize<Ident, "unique"> }
239+
kw_using { @specialize<Ident, "using"> }
240+
kw_var { @specialize<Ident, "var"> }
241+
kw_void { @specialize<Ident, "void"> }
242+
kw_while { @specialize<Ident, "while"> }
243+
kw_with { @specialize<Ident, "with"> }
244+
kw_yield { @specialize<Ident, "yield"> }
248245

249246
@skip { Shebang | JSDoc | TripleSlash | LineComment | BlockComment }
250247

src/gen-lezer.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -902,19 +902,15 @@ export function generateLezer(grammar: LezerGrammar): LezerOutput {
902902

903903
const ruleTexts: string[] = [];
904904
const entry = findEntryRule(grammar);
905-
// Lezer needs a top-level `@top` rule. Reuse the grammar's entry rule.
906-
ruleTexts.push(`@top Program { ${entry} }`);
907-
if (entry !== 'Program') {
908-
// entry rule will also be emitted below as a normal rule
909-
}
910-
905+
// Lezer needs exactly one `@top` rule. PROMOTE the grammar's entry rule to @top
906+
// (prefix its own definition) rather than emitting a separate wrapper — a wrapper
907+
// named `Program` would collide with an entry rule that is itself named `Program`.
911908
for (const rule of grammar.rules) {
912909
const localCtx = { ...exprCtx, selfName: rule.name };
913-
if (prattRuleNames.has(rule.name)) {
914-
ruleTexts.push(emitPrattRule(rule, grammar, prec, localCtx, incomplete));
915-
} else {
916-
ruleTexts.push(emitPlainRule(rule, localCtx));
917-
}
910+
const text = prattRuleNames.has(rule.name)
911+
? emitPrattRule(rule, grammar, prec, localCtx, incomplete)
912+
: emitPlainRule(rule, localCtx);
913+
ruleTexts.push(rule.name === entry ? `@top ${text}` : text);
918914
}
919915

920916
// ── Keyword specialization ──
@@ -928,10 +924,12 @@ export function generateLezer(grammar: LezerGrammar): LezerOutput {
928924

929925
const specializeLines: string[] = [];
930926
if (identToken && keywordLits.size > 0) {
931-
const mapping = [...keywordLits].sort()
932-
.map(kw => ` ${lezerString(kw)} ${keywordSpecialName(kw)}`)
933-
.join(',\n');
934-
specializeLines.push(`@specialize<${identToken.name}, keyword> {\n${mapping}\n}`);
927+
// Lezer has no `@specialize<tok, name> { … }` block form — each keyword is its own
928+
// rule that specializes the identifier token (the inline `@specialize<tok, "word">`
929+
// expression, as in @lezer/javascript's `kw<term>` template).
930+
for (const kw of [...keywordLits].sort()) {
931+
specializeLines.push(`${keywordSpecialName(kw)} { @specialize<${identToken.name}, ${lezerString(kw)}> }`);
932+
}
935933
} else if (keywordLits.size > 0) {
936934
incomplete.push('Keywords present but no @identifier token to @specialize over.');
937935
}

0 commit comments

Comments
 (0)