Skip to content

Commit 1664a49

Browse files
committed
test-issues: lock in 10 verified backlog issues (solved 39→49)
Verified 10 ledger-backlog issues are already correct in the generated TextMate grammar and added regression cases (310 checks pass, was 288): #1021 regex `v` flag, #788 optional chaining, #1025 for-of/in without spaces, #815 method named `new`, #881 `override` modifier, #1066 triple-slash directive, #992 type named `type`, #995 `as keyof typeof`, #994 default type-arg value, #1027 nested `>>`. Monogram now solves 49 of the 106 open official issues (~46%). Ledger + README updated; backlog trimmed to ~7, with #891 (binding named `from` mis-scoped as a keyword) and #1071 (contested import.meta) documented as the honest residual gaps.
1 parent 96d3e96 commit 1664a49

3 files changed

Lines changed: 107 additions & 15 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Take `typeof x < y`. A regex highlighter has to guess whether `<` opens a generi
3535

3636
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.
3737

38-
That's the categorical part: a highlighter derived from a parser-proven grammar isn't *a better hand-written grammar* — it's playing a different game. You can't out-regex it, because its correctness comes from a dimension hand-written grammars never operate in. The evidence is concrete — [`test/test-issues.ts`](test/test-issues.ts) replays **39 real bugs** from the official grammar's issue tracker (the `typeof x < y` ambiguities, regex-after-keyword cases, `as`-casts inside `<>`), all **288** token checks pass, and **all 39 are still open upstream** — Monogram already fixes ~37% of the 106 open official issues, because those failure modes are *structurally precluded* by a parser rather than patched one regex at a time. The [**upstream issue ledger**](docs/upstream-issues.md) tracks exactly which issues we solve and gives an honest verdict on each one we don't (backlog / out-of-scope / needs-semantics / proven-TM-impossible).
38+
That's the categorical part: a highlighter derived from a parser-proven grammar isn't *a better hand-written grammar* — it's playing a different game. You can't out-regex it, because its correctness comes from a dimension hand-written grammars never operate in. The evidence is concrete — [`test/test-issues.ts`](test/test-issues.ts) replays **49 real bugs** from the official grammar's issue tracker (the `typeof x < y` ambiguities, regex-after-keyword cases, `as`-casts inside `<>`, nested `>>`), all **310** token checks pass, and **all 49 are still open upstream** — Monogram already fixes ~46% of the 106 open official issues, because those failure modes are *structurally precluded* by a parser rather than patched one regex at a time. The [**upstream issue ledger**](docs/upstream-issues.md) tracks exactly which issues we solve and gives an honest verdict on each one we don't (backlog / out-of-scope / needs-semantics / proven-TM-impossible).
3939

4040
## Results
4141

@@ -45,7 +45,7 @@ Measured against the TypeScript compiler's own conformance suite (single-file ca
4545
Valid-code coverage 100% 3376 / 3376 valid single-file cases parse (zero gaps; no valid code missed)
4646
Bidirectional 97.8% 3585 / 3664 — also rejects what tsc rejects (gap = over-acceptance only)
4747
Highlighter 99.3% 589 / 593 tokens match VS Code's official grammar
48-
Official-grammar bugs 39 real TypeScript-TmLanguage issues replayed (288 token checks) — all pass
48+
Official-grammar bugs 49 real TypeScript-TmLanguage issues replayed (310 token checks) — all pass
4949
Source size 628 lines — 5× fewer than the official 3331-line hand-written TextMate YAML
5050
Engine language-agnostic — zero TypeScript-specific code (proven by test/agnostic.ts)
5151
```
@@ -149,7 +149,7 @@ Self-contained (no external setup):
149149
```bash
150150
node test/sanity-check.ts # quick smoke test
151151
node test/agnostic.ts # proves the engine is language-agnostic
152-
node test/test-issues.ts # replays 39 official-grammar bugs against the generated grammar
152+
node test/test-issues.ts # replays 49 official-grammar bugs against the generated grammar
153153
```
154154

155155
The conformance and coverage suites need external corpora and are **excluded from CI** for that reason:

docs/upstream-issues.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@ A maintained accounting of [microsoft/TypeScript-TmLanguage](https://github.com/
88

99
| Verdict | Count | Meaning |
1010
|---|---:|---|
11-
|**Solved** | **39** | parser-derived TM gets it right; replayed green in [`test/test-issues.ts`](../test/test-issues.ts) (288 checks). **All 39 are still open upstream.** |
12-
| 🔧 Backlog (our domain) | ~17 | TM-expressible + parser-derivable; not done yet. Some likely already correct, just untested. |
11+
|**Solved** | **49** | parser-derived TM gets it right; replayed green in [`test/test-issues.ts`](../test/test-issues.ts) (310 checks). **All 49 are still open upstream.** |
12+
| 🔧 Backlog (our domain) | ~7 | TM-expressible + parser-derivable; not done yet, or a real residual gap. |
1313
| 📦 Out of scope (now) | 8 + 16 | TSX dialect (8); JSDoc *embedded-content* highlighting (16) — needs the embedded-grammar pipeline. |
1414
| 🧠 Needs semantics | ~6 | type/symbol resolution — **no syntax highlighter (TM or parser) can do this**; the language server's job. |
1515
| 🛠 Not a grammar concern | ~20 | theming, semantic-token config, scope-name data, tooling, IDE support. |
1616
| ⛔ Proven TM-impossible | 0 confirmed | none yet proven (requires a failing generated-TM + a passing parser-target — see discipline). |
1717

18-
The headline: **Monogram already solves 39 of the 106 still-open official issues (~37%)** and they are exactly the regex-vs-division / generic-vs-comparison / multiline-context class that a hand-written grammar perpetually loses to.
18+
The headline: **Monogram already solves 49 of the 106 still-open official issues (~46%)** — exactly the regex-vs-division / generic-vs-comparison / multiline-context class that a hand-written grammar perpetually loses to.
1919

2020
---
2121

22-
## ✅ Solved (39)
22+
## ✅ Solved (49)
2323

2424
All replayed in [`test/test-issues.ts`](../test/test-issues.ts) and all still open upstream. Grouped by the ambiguity class — every one is something a regex grammar cannot reliably decide but a parser settles for free.
2525

2626
**Regex literal vs. division `/`** — the lexer uses parser context (what the previous token *is*) to decide:
27-
`#853` (after `throw`/`void`/`typeof`/`await`/`yield`), `#804` (`/[a\-b]/` char class), `#1024` (regex on a new line), `#883` (`for (const x of /re/.exec…)`), `#1055` (backtick inside a regex), `#1063` (`\cJ`/`\cj` control-char escapes).
27+
`#853` (after `throw`/`void`/`typeof`/`await`/`yield`), `#804` (`/[a\-b]/` char class), `#1024` (regex on a new line), `#883` (`for (const x of /re/.exec…)`), `#1055` (backtick inside a regex), `#1063` (`\cJ`/`\cj` control-char escapes), `#1021` (the `v` unicode-sets flag).
2828

2929
**Generic type-args `<…>` vs. less-than `<`** — the parser knows when `<` opens type arguments:
30-
`#1050`, `#978` (`typeof x < y`), `#859` (`as` cast inside `<>`), `#884` (`0 < x && 1 > 0`), `#904` (`bad < obj.two`), `#1020` (`new Map<…>` with no parens), `#855` (`<` with a `/* comment */` inside the args).
30+
`#1050`, `#978` (`typeof x < y`), `#859` (`as` cast inside `<>`), `#884` (`0 < x && 1 > 0`), `#904` (`bad < obj.two`), `#1020` (`new Map<…>` with no parens), `#855` (`<` with a `/* comment */` inside the args), `#1027` (nested `>>` closes two arg-lists, not a shift), `#994` (default type-arg *value* is colored), `#995` (paren-wrapped `as keyof typeof`), `#992` (a type *named* `type`).
3131

3232
**Multiline / whitespace-fragile generics, types, unions** — structure, not column position, decides; a later construct never "breaks":
3333
`#1002`, `#1014`, `#1019`, `#1028`, `#1035`, `#1040`, `#1041`, `#1043`, `#1051`, `#1053`, `#1056`, `#1059`, `#819`, `#876`, `#889`, `#890`, `#894`, `#896`, `#911`, `#973`, `#981`, `#983`, `#873`.
3434

35-
**Member / call scoping & control flow:** `#736` (method call → `entity.name.function`), `#770` (call parens are punctuation), `#869` (`x in obj ? a : b` ternary).
35+
**Member / call scoping, keywords-as-names, control flow:** `#736` (method call → `entity.name.function`), `#770` (call parens are punctuation), `#869` (`x in obj ? a : b` ternary), `#788` (optional chaining `?.`), `#1025` (`for…of`/`in` with no surrounding space), `#815` (a method *named* `new`), `#881` (`override` modifier), `#1066` (triple-slash `/// <reference>` directive).
3636

3737
> Each maps to one or more cases in `test/test-issues.ts`; grep the issue number there for the exact input + expected scopes.
3838
@@ -49,13 +49,15 @@ When an issue is **not** solved, it gets exactly one verdict, and **⛔ TM-impos
4949

5050
---
5151

52-
## 🔧 Backlog — our domain, solvable, not yet (≈17)
52+
## 🔧 Backlog — our domain, not yet solved (≈7)
5353

54-
TM-expressible parse/lex disambiguation, squarely in Monogram's wheelhouse. Several are probably already correct in the generated grammar and only need a test case to confirm and move to Solved:
54+
TM-expressible parse/lex disambiguation in Monogram's wheelhouse, still open after the verification pass (10 candidates that *were* here are now confirmed-correct and moved to Solved):
5555

56-
`#1071` `import.meta.dirname` (`import` mis-keyworded), `#1048` multiline `extends` in a generic with split parens, `#1027` color inside angle brackets, `#1025` `for-of`/`in` with no surrounding space, `#1021` regex `v` flag *(our `Regex` token already lists `v` — likely solved, verify)*, `#995` `(obj as keyof typeof X)` paren-wrapped assertion, `#994` default template argument color, `#992` cast on a variable named `type`, `#891` default import named `from` *(we parse `import type from "…"` — likely solved)*, `#881` `override` modifier, `#815` method named `new`, `#857` multiple `export default` overloads, `#497` `import` declaration / semicolon scoping, `#810` trailing newline in a line comment, `#788` optional chaining *(supported — verify)*, `#1039` `String.raw` escape tokens, `#1066` triple-slash reference directive.
56+
- `#891` default import *named* `from` (`import from from "x"`) — **confirmed gap:** the binding `from` is mis-scoped as `keyword.control.import` instead of a variable, because `from` is in the keyword scope-map regardless of position. Needs the import-clause name to override the keyword scope. (Doesn't *break* the rest, unlike upstream — but the binding color is wrong.)
57+
- `#1071` `import.meta.dirname`**contested:** we scope `import` as `keyword.control.import` (defensible — it *is* the import meta-property keyword) and `meta`/`dirname` as properties, with no breakage; the reporter wants `import` non-keyworded. Left until the intended scope is settled.
58+
- `#1048` multiline `extends` in a generic with split parens; `#857` multiple `export default` overloads; `#497` `import` declaration / semicolon scoping; `#810` trailing newline scope in a line comment; `#1039` `String.raw` escape-token visibility.
5759

58-
> Action: each should be run through the generated TM (the `test/test-issues.ts` harness) to confirm pass/fail before claiming. Listing here ≠ "broken in Monogram" — it means "not yet verified-and-tested."
60+
> Action: run each through `test/test-issues.ts` to confirm before claiming. Listing here ≠ "breaks in Monogram" — it means "not yet correct-and-tested."
5961
6062
## 📦 Out of current scope
6163

@@ -81,7 +83,7 @@ Theming / token-color config (`#1064`, `#991`), scope-name data & spelling fixes
8183
### Proof B
8284
**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.
8385

84-
> **But beware over-claiming this.** The 39 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."
86+
> **But beware over-claiming this.** The 49 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."
8587
8688
---
8789

test/test-issues.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,96 @@ const tests: TestCase[] = [
573573
{ text: '`', scope: 'punctuation.definition.string.template.end' },
574574
],
575575
},
576+
577+
// ── Backlog issues confirmed already-correct in the generated grammar (verified, then locked in) ──
578+
{
579+
label: '#1021: regex with the v (unicode-sets) flag is recognized',
580+
input: 'const re = /foo/v;',
581+
checks: [
582+
{ text: 'foo', scope: 'string.regexp' },
583+
{ text: 'v', scope: 'keyword.other.regexp' },
584+
],
585+
},
586+
{
587+
label: '#788: optional chaining ?. is the optional accessor',
588+
input: 'a?.b;',
589+
checks: [
590+
{ text: '?.', scope: 'punctuation.accessor.optional' },
591+
{ text: 'b', scope: 'variable.other' },
592+
],
593+
},
594+
{
595+
label: '#1025: for-of without surrounding space keeps `of` a loop keyword',
596+
input: 'for(const x of[1]);',
597+
checks: [
598+
{ text: 'of', scope: 'keyword.control.loop' },
599+
],
600+
},
601+
{
602+
label: '#1025: for-in without surrounding space keeps `in` a loop keyword',
603+
input: 'for(const k in o);',
604+
checks: [
605+
{ text: 'in', scope: 'keyword.control.loop' },
606+
],
607+
},
608+
{
609+
label: '#815: a class method named `new` is a method name, not the operator',
610+
input: 'class C { new() {} }',
611+
checks: [
612+
{ text: 'new', scope: 'entity.name.function' },
613+
],
614+
},
615+
{
616+
label: '#881: `override` modifier on a method is storage.modifier',
617+
input: 'class C extends B { override f() {} }',
618+
checks: [
619+
{ text: 'override', scope: 'storage.modifier' },
620+
{ text: 'f', scope: 'entity.name.function' },
621+
],
622+
},
623+
{
624+
label: '#1066: triple-slash reference directive is a comment',
625+
input: `/// <reference path="x.d.ts" />`,
626+
checks: [
627+
{ text: `/// <reference path="x.d.ts" />`, scope: 'comment.line.triple-slash' },
628+
],
629+
},
630+
{
631+
label: '#992: casting to a type named `type` does not break highlighting',
632+
input: 'const y = (x as type).z;',
633+
checks: [
634+
{ text: 'as', scope: 'keyword.operator.expression' },
635+
{ text: 'type', scope: 'entity.name.type' },
636+
{ text: 'z', scope: 'entity.other.property' },
637+
],
638+
},
639+
{
640+
label: '#995: paren-wrapped `as keyof typeof` assertion tokenizes',
641+
input: 'const k = (obj as keyof typeof X);',
642+
checks: [
643+
{ text: 'as', scope: 'keyword.operator.expression' },
644+
{ text: 'keyof', scope: 'keyword.operator.expression.keyof' },
645+
{ text: 'typeof', scope: 'keyword.operator.expression' },
646+
{ text: 'X', scope: 'variable.other' },
647+
],
648+
},
649+
{
650+
label: '#994: default type-parameter value is colored',
651+
input: 'function f<T = string>(): T {}',
652+
checks: [
653+
{ text: 'T', scope: 'entity.name.type' },
654+
{ text: 'string', scope: 'support.type.primitive' },
655+
],
656+
},
657+
{
658+
label: '#1027: nested generic `>>` closes two type-arg lists, not a shift',
659+
input: 'let m: Map<string, Array<number>>;',
660+
checks: [
661+
{ text: 'Map', scope: 'entity.name.type' },
662+
{ text: 'Array', scope: 'entity.name.type' },
663+
{ text: 'number', scope: 'support.type.primitive' },
664+
],
665+
},
576666
];
577667

578668
// ══════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)