Skip to content

Commit 9c30cfd

Browse files
committed
Fix variance type-param regression; regenerate artifacts (3602/3776)
A type parameter named `in`/`out` preceded by a variance modifier (`type T1<in in>`, `type T2<out out>`) failed to parse: the greedy `many1(mod)` consumed both keywords, leaving no name and no backtrack. TS treats the last `in`/`out` before the terminator as the name. Add a single-modifier `[mod, name]` branch where the name may itself be a contextual variance keyword, and let longest-match arbitrate against the modifier-soup and bare-name branches. Verified no regressions across the conformance suite. Regenerate all derived targets (TextMate, tree-sitter, Lezer, Monarch) from the merged grammar and update README conformance figures.
1 parent 558df24 commit 9c30cfd

7 files changed

Lines changed: 26 additions & 24 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A TextMate grammar is a pile of regexes guessing at a language's structure. It's
1010

1111
Monogram inverts the dependency:
1212

13-
1. **Write the grammar, then prove it.** The grammar is executable. Monogram runs it as a recursive-descent + Pratt parser over the TypeScript conformance suite. Today it parses **95.0%** (3589 / 3776 files); the goal is **100% coverage of the official parser** — at which point the grammar is a *verified, complete model* of the language's syntax, not an approximation.
13+
1. **Write the grammar, then prove it.** The grammar is executable. Monogram runs it as a recursive-descent + Pratt parser over the TypeScript conformance suite. Today it parses **95.4%** (3602 / 3776 files); the goal is **100% coverage of the official parser** — at which point the grammar is a *verified, complete model* of the language's syntax, not an approximation.
1414

1515
2. **Derive the highlighter from the proven grammar.** The TextMate grammar is generated from that same parser-validated grammar — never hand-written. Its correctness is underwritten by the parser conformance run, not by regex tuning.
1616

@@ -37,7 +37,7 @@ And — from the same grammar — first-pass generators for the rest of the edit
3737
Validated on TypeScript (grammar: [`examples/typescript.ts`](examples/typescript.ts), 537 lines):
3838

3939
```
40-
Parser conformance 95.0% 3589 / 3776 files from the TypeScript conformance suite (goal: 100%)
40+
Parser conformance 95.4% 3602 / 3776 files from the TypeScript conformance suite (goal: 100%)
4141
Highlighter 99.3% 589 / 593 tokens match VS Code's official grammar
4242
Generated grammar 42 KB vs the official hand-written 226 KB
4343
Engine language-agnostic — no TypeScript-specific code
@@ -184,4 +184,4 @@ Every highlighter target (TextMate, tree-sitter queries, Lezer styleTags, Monarc
184184
| ANTLR | yes |||
185185
| Langium | yes | Monarch (separate) ||
186186
| ungrammar | AST types |||
187-
| **Monogram** | **CST (95.0% → 100%)** | **auto-derived (99.3%)** | **yes** |
187+
| **Monogram** | **CST (95.4% → 100%)** | **auto-derived (99.3%)** | **yes** |

examples/lezer/tokens.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010
// This module must also supply these tokens that exceeded Lezer's @tokens syntax
1111
// (straightforward longest-match scanners — INCOMPLETE, regex listed for porting):
12-
// JSDoc: /\/\*\*[\s\S]*?\*\// (skip)
12+
// JSDoc: /\/\*\*(?!\/)[\s\S]*?\*\// (skip)
1313
// BlockComment: /\/\*[\s\S]*?\*\// (skip)
1414
// Ident: /(?:[a-zA-Z_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})(?:[a-zA-Z0-9_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})*/
1515
// Number: /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?/

examples/lezer/typescript.grammar

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Type {
2727
(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)
2828
}
2929
TypeMember {
30-
(kw_new? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Op_2b | Op_2d)? kw_readonly? BracketL (Ident (kw_in Type (kw_as Type)? BracketR (Op_2b | Op_2d)? Question? Colon Type | Colon Type BracketR (Colon Type)?) | Expr BracketR Question? (TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Colon Type)?)) | kw_readonly Ident Question? Colon Type | (Ident | Number | String | PrivateField) Question? (TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Colon Type)?))
30+
(kw_new? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Op_2b | Op_2d)? kw_readonly? BracketL (Ident (kw_in Type (kw_as Type)? BracketR (Op_2b | Op_2d)? Question? Colon Type | Colon Type BracketR (Colon Type)?) | Expr BracketR Question? (TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Colon Type)?) | BracketR (Colon Type)?) | kw_readonly Ident Question? Colon Type | (Ident | Number | String | PrivateField) Question? (TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? | (Colon Type)?))
3131
}
3232
DecoratorExpr {
3333
Decorator (ParenL (Expr (Comma Expr)*)? ParenR | AngleL (Type (Comma Type)*)? AngleR ParenL (Expr (Comma Expr)*)? ParenR)?
@@ -48,7 +48,7 @@ Expr {
4848
kw_this |
4949
kw_super |
5050
Spread Expr |
51-
Expr AngleL (Type (Comma Type)*)? AngleR (ParenL (Expr (Comma Expr)*)? ParenR | Template)? |
51+
Expr AngleL (Type (Comma Type)*)? AngleR (ParenL (Expr (Comma Expr)*)? ParenR | Template | ) |
5252
Expr ParenL (Expr (Comma Expr)*)? ParenR |
5353
Expr Dot (Ident | PrivateField) |
5454
Expr QuestionDot (Ident | PrivateField | ParenL (Expr (Comma Expr)*)? ParenR | BracketL Expr BracketR | Template) |
@@ -145,7 +145,7 @@ TypeParams {
145145
AngleL (TypeParam (Comma TypeParam)*)? AngleR
146146
}
147147
TypeParam {
148-
((kw_const | kw_in | kw_out | kw_public | kw_private | kw_protected | kw_readonly)+ Ident (kw_extends Type)? (Op_3d Type)? | Ident (kw_extends Type)? (Op_3d Type)?)
148+
((kw_const | kw_in | kw_out | kw_public | kw_private | kw_protected | kw_readonly)+ Ident (kw_extends Type)? (Op_3d Type)? | (kw_const | kw_in | kw_out | kw_public | kw_private | kw_protected | kw_readonly) (Ident | kw_in | kw_out) (kw_extends Type)? (Op_3d Type)? | (Ident | kw_in | kw_out) (kw_extends Type)? (Op_3d Type)?)
149149
}
150150
Decl {
151151
(kw_async? kw_function Op_2a? Ident TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? (Block | Semi?) | kw_interface Ident TypeParams? (kw_extends (Type (Comma Type)*)?)? BraceL (InterfaceMember (Semi | Comma)?)* BraceR | kw_type Ident TypeParams? Op_3d Type Semi? | DecoratorExpr* kw_abstract? kw_class Ident TypeParams? (kw_extends ClassHeritage)? (kw_implements (Type (Comma Type)*)?)? BraceL ClassMember* BraceR | kw_enum Ident BraceL (EnumMember (Comma EnumMember)*)? BraceR | kw_declare kw_function Op_2a? Ident TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Semi? | kw_declare (Decl | Stmt) | kw_namespace Ident BraceL Stmt* BraceR | kw_module (String | Ident) BraceL Stmt* BraceR | kw_export (Decl | Stmt) | kw_export kw_default (kw_async? kw_function Op_2a? Ident? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? (Block | Semi?) | kw_abstract kw_class Ident TypeParams? (kw_extends ClassHeritage)? (kw_implements (Type (Comma Type)*)?)? BraceL ClassMember* BraceR | kw_abstract kw_class TypeParams? (kw_extends ClassHeritage)? (kw_implements (Type (Comma Type)*)?)? BraceL ClassMember* BraceR | Expr Semi?) | kw_export Op_2a (kw_from String Semi? | kw_as Ident kw_from String Semi?) | kw_export BraceL (ImportSpecifier (Comma ImportSpecifier)*)? BraceR (kw_from String)? Semi? | kw_export Op_3d Expr Semi? | kw_export kw_type BraceL (ImportSpecifier (Comma ImportSpecifier)*)? BraceR (kw_from String)? Semi? | kw_const kw_enum Ident BraceL (EnumMember (Comma EnumMember)*)? BraceR | kw_import (ImportClause kw_from String Semi? | kw_type ImportClause kw_from String Semi? | Ident Op_3d Expr Semi? | String Semi?) | DecoratorExpr* kw_export (Decl | Stmt))
@@ -248,7 +248,7 @@ Program {
248248

249249
@tokens {
250250
Shebang { "#!"![\n]* }
251-
// INCOMPLETE: JSDoc block span "\\/\\*\\*"…"\\*\\/" → external tokenizer.
251+
// INCOMPLETE: JSDoc block span "\\/\\*\\*(?!\\/)"…"\\*\\/" → external tokenizer.
252252
TripleSlash { "/""/""/"$[ \t\n\r]*"<"![\n]* }
253253
LineComment { "/""/"![\n]* }
254254
// INCOMPLETE: BlockComment block span "\\/\\*"…"\\*\\/" → external tokenizer.
@@ -337,5 +337,5 @@ Program {
337337

338338
// ── INCOMPLETE / hand-tuning notes ──
339339
// - Rule Expr: Pratt operators (op/prefix/postfix) mapped to precedence-marked binary/unary alternatives. Lezer is GLR + @precedence, not Pratt, so deeply overloaded operators (e.g. '<' as both comparison and type-args, '+' infix vs prefix) may need hand-tuning or the external tokenizer to fully disambiguate.
340-
// - Block-style token JSDoc (delimiters "\\/\\*\\*"…"\\*\\/") routed to the external tokenizer; Lezer single tokens can't longest-match a multi-char close delimiter.
340+
// - Block-style token JSDoc (delimiters "\\/\\*\\*(?!\\/)"…"\\*\\/") routed to the external tokenizer; Lezer single tokens can't longest-match a multi-char close delimiter.
341341
// - Block-style token BlockComment (delimiters "\\/\\*"…"\\*\\/") routed to the external tokenizer; Lezer single tokens can't longest-match a multi-char close delimiter.

examples/tree-sitter/grammar.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module.exports = grammar({
3636

3737
type: $ => choice(seq($.ident, optional(seq("is", $.type))), seq($.type, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">"), seq($.type, "[", "]"), seq($.type, "|", $.type), seq($.type, "&", $.type), seq("|", $.type), seq("&", $.type), seq("keyof", $.type), seq("typeof", $.typeof_ref), seq("readonly", $.type), seq("(", $.type, ")"), seq(optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", "=>", $.type), seq(optional("abstract"), "new", optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", "=>", $.type), seq("[", repeat(seq(optional("..."), optional(seq($.ident, optional("?"), ":")), optional("..."), $.type, optional("?"), optional(","))), "]"), seq("{", repeat(seq($.type_member, optional(choice(";", ",")))), "}"), seq("asserts", $.ident, optional(seq("is", $.type))), seq($.type, "extends", $.type, "?", $.type, ":", $.type), seq("infer", $.ident, optional(seq("extends", $.type, optional(seq("?", $.type, ":", $.type))))), $.string, $.number, $.hex_number, $.octal_number, $.binary_number, $.big_int, seq("-", choice($.number, $.big_int)), "true", "false", "null", "undefined", "void", "this", seq("unique", "symbol"), seq("import", "(", $.type, ")"), $.template, seq($.type, "[", $.type, "]"), seq($.type, ".", $.ident)),
3838

39-
type_member: $ => choice(seq(optional("new"), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), seq(optional(choice("+", "-")), optional("readonly"), "[", choice(seq($.ident, choice(seq("in", $.type, optional(seq("as", $.type)), "]", optional(choice("+", "-")), optional("?"), ":", $.type), seq(":", $.type, "]", optional(seq(":", $.type))))), seq($.expr, "]", optional("?"), choice(seq(optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), optional(seq(":", $.type)))))), seq("readonly", $.ident, optional("?"), ":", $.type), seq(choice($.ident, $.number, $.string, $.private_field), optional("?"), choice(seq(optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), optional(seq(":", $.type))))),
39+
type_member: $ => choice(seq(optional("new"), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), seq(optional(choice("+", "-")), optional("readonly"), "[", choice(seq($.ident, choice(seq("in", $.type, optional(seq("as", $.type)), "]", optional(choice("+", "-")), optional("?"), ":", $.type), seq(":", $.type, "]", optional(seq(":", $.type))))), seq($.expr, "]", optional("?"), choice(seq(optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), optional(seq(":", $.type)))), seq("]", optional(seq(":", $.type))))), seq("readonly", $.ident, optional("?"), ":", $.type), seq(choice($.ident, $.number, $.string, $.private_field), optional("?"), choice(seq(optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type))), optional(seq(":", $.type))))),
4040

4141
decorator_expr: $ => seq($.decorator, optional(choice(seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), seq("<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">", "(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")")))),
4242

@@ -72,7 +72,7 @@ module.exports = grammar({
7272
"this",
7373
"super",
7474
seq("...", $.expr),
75-
prec.left(18, seq($.expr, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">", optional(choice(seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), $.template)))),
75+
prec.left(18, seq($.expr, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">", choice(seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), $.template, blank()))),
7676
prec.left(18, seq($.expr, "(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")")),
7777
prec.left(18, seq($.expr, ".", choice($.ident, $.private_field))),
7878
prec.left(18, seq($.expr, "?.", choice($.ident, $.private_field, seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), seq("[", $.expr, "]"), $.template))),
@@ -136,7 +136,7 @@ module.exports = grammar({
136136

137137
type_params: $ => seq("<", optional(seq($.type_param, repeat(seq(",", $.type_param)), optional(","))), ">"),
138138

139-
type_param: $ => choice(seq(repeat1(choice("const", "in", "out", "public", "private", "protected", "readonly")), $.ident, optional(seq("extends", $.type)), optional(seq("=", $.type))), seq($.ident, optional(seq("extends", $.type)), optional(seq("=", $.type)))),
139+
type_param: $ => choice(seq(repeat1(choice("const", "in", "out", "public", "private", "protected", "readonly")), $.ident, optional(seq("extends", $.type)), optional(seq("=", $.type))), seq(choice("const", "in", "out", "public", "private", "protected", "readonly"), choice($.ident, "in", "out"), optional(seq("extends", $.type)), optional(seq("=", $.type))), seq(choice($.ident, "in", "out"), optional(seq("extends", $.type)), optional(seq("=", $.type)))),
140140

141141
decl: $ => choice(seq(optional("async"), "function", optional("*"), field('name', $.ident), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), choice($.block, optional(";"))), seq("interface", field('name', $.ident), optional($.type_params), optional(seq("extends", optional(seq($.type, repeat(seq(",", $.type)), optional(","))))), "{", repeat(seq($.interface_member, optional(choice(";", ",")))), "}"), seq("type", field('name', $.ident), optional($.type_params), "=", $.type, optional(";")), seq(repeat($.decorator_expr), optional("abstract"), "class", field('name', $.ident), optional($.type_params), optional(seq("extends", $.class_heritage)), optional(seq("implements", optional(seq($.type, repeat(seq(",", $.type)), optional(","))))), "{", repeat($.class_member), "}"), seq("enum", field('name', $.ident), "{", optional(seq($.enum_member, repeat(seq(",", $.enum_member)), optional(","))), "}"), seq("declare", "function", optional("*"), field('name', $.ident), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), optional(";")), seq("declare", choice($.decl, $.stmt)), seq("namespace", field('name', $.ident), "{", repeat($.stmt), "}"), seq("module", choice($.string, $.ident), "{", repeat($.stmt), "}"), seq("export", choice($.decl, $.stmt)), seq("export", "default", choice(seq(optional("async"), "function", optional("*"), optional(field('name', $.ident)), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), choice($.block, optional(";"))), seq("abstract", "class", field('name', $.ident), optional($.type_params), optional(seq("extends", $.class_heritage)), optional(seq("implements", optional(seq($.type, repeat(seq(",", $.type)), optional(","))))), "{", repeat($.class_member), "}"), seq("abstract", "class", optional($.type_params), optional(seq("extends", $.class_heritage)), optional(seq("implements", optional(seq($.type, repeat(seq(",", $.type)), optional(","))))), "{", repeat($.class_member), "}"), seq($.expr, optional(";")))), seq("export", "*", choice(seq("from", $.string, optional(";")), seq("as", $.ident, "from", $.string, optional(";")))), seq("export", "{", optional(seq($.import_specifier, repeat(seq(",", $.import_specifier)), optional(","))), "}", optional(seq("from", $.string)), optional(";")), seq("export", "=", $.expr, optional(";")), seq("export", "type", "{", optional(seq($.import_specifier, repeat(seq(",", $.import_specifier)), optional(","))), "}", optional(seq("from", $.string)), optional(";")), seq("const", "enum", field('name', $.ident), "{", optional(seq($.enum_member, repeat(seq(",", $.enum_member)), optional(","))), "}"), seq("import", choice(seq($.import_clause, "from", $.string, optional(";")), seq("type", $.import_clause, "from", $.string, optional(";")), seq($.ident, "=", $.expr, optional(";")), seq($.string, optional(";")))), seq(repeat($.decorator_expr), "export", choice($.decl, $.stmt))),
142142

@@ -152,7 +152,7 @@ module.exports = grammar({
152152

153153
shebang: $ => token(/^#![^\n]*/),
154154

155-
jsdoc: $ => token(/\/\*\*[\s\S]*?\*\//),
155+
jsdoc: $ => token(/\/\*\*(?!\/)[\s\S]*?\*\//),
156156

157157
triple_slash: $ => token(/\/\/\/\s*<[^\n]*/),
158158

examples/typescript.monarch.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
"white"
186186
],
187187
[
188-
"\\/\\*\\*",
188+
"\\/\\*\\*(?!\\/)",
189189
{
190190
"token": "comment",
191191
"next": "@comment_slashstarstar"
@@ -251,7 +251,7 @@
251251
"white"
252252
],
253253
[
254-
"\\/\\*\\*",
254+
"\\/\\*\\*(?!\\/)",
255255
{
256256
"token": "comment",
257257
"next": "@comment_slashstarstar"
@@ -317,7 +317,7 @@
317317
"white"
318318
],
319319
[
320-
"\\/\\*\\*",
320+
"\\/\\*\\*(?!\\/)",
321321
{
322322
"token": "comment",
323323
"next": "@comment_slashstarstar"
@@ -477,7 +477,7 @@
477477
"white"
478478
],
479479
[
480-
"\\/\\*\\*",
480+
"\\/\\*\\*(?!\\/)",
481481
{
482482
"token": "comment",
483483
"next": "@comment_slashstarstar"

examples/typescript.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@
411411
},
412412
"jsdoc": {
413413
"name": "comment.block.documentation.typescript",
414-
"begin": "\\/\\*\\*",
414+
"begin": "\\/\\*\\*(?!\\/)",
415415
"end": "\\*\\/",
416416
"contentName": "meta.embedded.block.jsdoc"
417417
},

examples/typescript.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,15 +312,17 @@ const Stmt = rule($ => [
312312

313313
const TypeParam = rule($ => {
314314
// TS parses any modifier soup before a type-param name (variance `in`/`out`,
315-
// `const`, even bogus `public`), then reports invalid ones post-parse. `many1`
316-
// requires a name AFTER the modifiers, so a param NAMED like a modifier (`<out>`,
317-
// `<in = any>`) falls through to the bare-name branch (longest-match: when a
318-
// name is present the modifier branch wins because it consumes more).
315+
// `const`, even bogus `public`), then reports invalid ones post-parse. A param
316+
// can also be NAMED like a modifier — `<in>`, `<out = any>`, and even the
317+
// variance-modified `<in in>` / `<out out>` (first `in`/`out` is the modifier,
318+
// second is the name). Longest-match picks among:
319319
const tail = [opt('extends', Type), opt('=', Type)];
320320
const mod = alt('const', 'in', 'out', 'public', 'private', 'protected', 'readonly');
321+
const name = alt(Ident, 'in', 'out'); // a name may itself be a contextual variance keyword
321322
return [
322-
[many1(mod), Ident, ...tail],
323-
[Ident, ...tail],
323+
[many1(mod), Ident, ...tail], // modifier soup + real-ident name: `<const in T>`, `<in T>`
324+
[mod, name, ...tail], // single modifier + in/out-named param: `<in in>`, `<out out>`
325+
[name, ...tail], // bare name, incl. `<in>`, `<out>`: `<T>`, `<in = any>`
324326
];
325327
});
326328

0 commit comments

Comments
 (0)