Skip to content

Commit 437a5ea

Browse files
Generative testing: grammar-derived inputs + by-construction consistency (#25) (#28)
1 parent 86fd02f commit 437a5ea

30 files changed

Lines changed: 2570 additions & 913 deletions

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ jobs:
5757
node test/vue-interp-expr.ts
5858
node test/yaml-issue12-regressions.ts
5959
node test/yaml-depth-witnesses.ts
60+
node test/generative.ts
61+
# The gap ledger is the deterministic, oracle-classified record of the divergences the
62+
# generative check DISCOVERS. Its self-test asserts the ddmin keep-path + the oracle
63+
# drop-path + determinism; --check fails if the committed KNOWN-GAPS.md is stale (the
64+
# ledger is a pure function of the grammar, so it must be regenerated when a grammar
65+
# changes — `npm run gap-ledger`). This is the deterministic source of truth; a later
66+
# layer can turn rows into issues, but the committed artifact is gated here first.
67+
node test/gap-ledger-selftest.ts
68+
node test/gap-ledger.ts --check
6069
6170
# The derived tree-sitter highlighter is the strongest thesis proof (a real GLR
6271
# parser from the same grammar, beating the official hand-written one). Build its

KNOWN-GAPS.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# KNOWN-GAPS — Monogram flat-highlighter divergences (auto-generated)
2+
3+
<!-- AUTO-GENERATED by `node test/gap-ledger.ts --write`. Do not edit by hand. -->
4+
5+
A **gap** is a position where, on **valid input** (accepted by the language’s external
6+
authority — typescript / yaml / parse5), the **flat TextMate highlighter** paints a token a
7+
different visual role than the **Monogram parser** assigns it by construction. These are the
8+
floor-blind divergences the generative scope≡role check (`test/generative.ts`) DISCOVERS over
9+
grammar-derived inputs — the monogram#23/#24 class — which the corpus-bound scope-gap metric is
10+
blind to (a small/clean corpus may never contain the shape, and the role-graded metric ignores
11+
punctuation-floor mis-paints). Each gap’s input is **minimized** (delta-debugged to a minimal
12+
repro that still parses and still diverges) and **fingerprinted** (a content hash, stable across
13+
commits) so the ledger is deterministic and commit-trackable.
14+
15+
Regenerate: `node test/gap-ledger.ts --write` · verify up-to-date: `node test/gap-ledger.ts --check`.
16+
17+
**2 gaps** across 7 grammars · 0 dropped.
18+
19+
## `525e867dc205` — html: #24 structural-literal→content
20+
21+
- **Language:** html
22+
- **Minimal repro:** `<A A=""/>`
23+
- **Divergent token:** `/` (parser token `$punct`)
24+
- **Role vs scope:** want **punct**, got **string** (highlighter scope `string.unquoted.html`)
25+
- **Fingerprint:** `525e867dc205`
26+
27+
```json
28+
{
29+
"id": "525e867dc205",
30+
"language": "html",
31+
"kind": "#24 structural-literal→content",
32+
"repro": "<A A=\"\"/>",
33+
"tokenType": "$punct",
34+
"tokenText": "/",
35+
"want": "punct",
36+
"got": "string",
37+
"gotScope": "string.unquoted.html"
38+
}
39+
```
40+
41+
## `85c793d02a86` — vue: #24 structural-literal→content
42+
43+
- **Language:** vue
44+
- **Minimal repro:** `<A A=""/>`
45+
- **Divergent token:** `/` (parser token `$punct`)
46+
- **Role vs scope:** want **punct**, got **string** (highlighter scope `string.unquoted.vue`)
47+
- **Fingerprint:** `85c793d02a86`
48+
49+
```json
50+
{
51+
"id": "85c793d02a86",
52+
"language": "vue",
53+
"kind": "#24 structural-literal→content",
54+
"repro": "<A A=\"\"/>",
55+
"tokenType": "$punct",
56+
"tokenText": "/",
57+
"want": "punct",
58+
"got": "string",
59+
"gotScope": "string.unquoted.vue"
60+
}
61+
```
62+

package.json

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"scripts": {
66
"gen": "node src/cli.ts typescript.ts && node src/cli.ts javascript.ts && node src/cli.ts typescriptreact.ts && node src/cli.ts javascriptreact.ts && node src/cli.ts html.ts && node src/cli.ts vue.ts && node src/cli.ts yaml.ts",
77
"test": "node test/sanity-check.ts",
8+
"generative": "node test/generative.ts",
9+
"gap-ledger": "node test/gap-ledger.ts --write",
10+
"gap-ledger:check": "node test/gap-ledger.ts --check",
11+
"gap-ledger:selftest": "node test/gap-ledger-selftest.ts",
812
"conformance": "node test/run-conformance.ts",
913
"conformance:js": "node test/js-conformance.ts",
1014
"conformance:tsx": "node test/tsx-conformance.ts",
@@ -30,19 +34,19 @@
3034
"bench:perf": "node test/perf-bench.ts",
3135
"coverage": "node test/scope-coverage.ts",
3236
"compat": "node test/repo-compat.ts",
33-
"src-coverage:ts": "node test/src-coverage-ts.ts",
34-
"src-coverage:js": "node test/src-coverage-js.ts",
35-
"src-coverage:jsx": "node test/src-coverage-jsx.ts",
36-
"src-coverage:tsx": "node test/src-coverage-tsx.ts",
37-
"src-coverage:html": "node test/src-coverage-html.ts",
38-
"src-coverage:yaml": "node test/src-coverage-yaml.ts",
39-
"scope-gap:ts": "node test/scope-gap-ts.ts",
40-
"scope-gap:js": "node test/scope-gap-js.ts",
41-
"scope-gap:jsx": "node test/scope-gap-jsx.ts",
42-
"scope-gap:tsx": "node test/scope-gap-tsx.ts",
43-
"scope-gap:html": "node test/scope-gap-html.ts",
44-
"scope-gap:yaml": "node test/scope-gap-yaml.ts",
45-
"scope-gap:vue": "node test/scope-gap-vue.ts",
37+
"src-coverage:ts": "node test/src-coverage-run.ts ts",
38+
"src-coverage:js": "node test/src-coverage-run.ts js",
39+
"src-coverage:jsx": "node test/src-coverage-run.ts jsx",
40+
"src-coverage:tsx": "node test/src-coverage-run.ts tsx",
41+
"src-coverage:html": "node test/src-coverage-run.ts html",
42+
"src-coverage:yaml": "node test/src-coverage-run.ts yaml",
43+
"scope-gap:ts": "node test/scope-gap-run.ts ts",
44+
"scope-gap:js": "node test/scope-gap-run.ts js",
45+
"scope-gap:jsx": "node test/scope-gap-run.ts jsx",
46+
"scope-gap:tsx": "node test/scope-gap-run.ts tsx",
47+
"scope-gap:html": "node test/scope-gap-run.ts html",
48+
"scope-gap:yaml": "node test/scope-gap-run.ts yaml",
49+
"scope-gap:vue": "node test/scope-gap-run.ts vue",
4650
"coverage:table": "node test/coverage-table.ts --write"
4751
},
4852
"devDependencies": {

test/bench-vs-ts-agg.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

test/bench-vs-ts.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

test/classify-ts.ts

Lines changed: 0 additions & 93 deletions
This file was deleted.

test/coverage-table.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,29 @@ function runAdapter(script: string, args: string[], marker: string, env?: NodeJS
1818
} catch { return null; }
1919
}
2020

21-
// TS/JS use deterministic stride subsets for speed; the rest run their full corpus.
21+
// Both metrics now run through ONE data-driven driver each, parameterised by the `<lang>` code
22+
// (test/scope-gap-run.ts, test/src-coverage-run.ts). TS/JS use deterministic stride subsets for
23+
// speed; the rest run their full corpus.
2224
const COV = [
23-
{ lang: 'TypeScript', script: 'test/src-coverage-ts.ts', args: ['1500'] },
24-
{ lang: 'JavaScript', script: 'test/src-coverage-js.ts', args: ['800'] },
25-
{ lang: 'JSX', script: 'test/src-coverage-jsx.ts', args: [] },
26-
{ lang: 'TSX', script: 'test/src-coverage-tsx.ts', args: [] },
27-
{ lang: 'HTML', script: 'test/src-coverage-html.ts', args: [] },
28-
{ lang: 'YAML', script: 'test/src-coverage-yaml.ts', args: [] },
25+
{ lang: 'TypeScript', script: 'test/src-coverage-run.ts', args: ['ts', '1500'] },
26+
{ lang: 'JavaScript', script: 'test/src-coverage-run.ts', args: ['js', '800'] },
27+
{ lang: 'JSX', script: 'test/src-coverage-run.ts', args: ['jsx'] },
28+
{ lang: 'TSX', script: 'test/src-coverage-run.ts', args: ['tsx'] },
29+
{ lang: 'HTML', script: 'test/src-coverage-run.ts', args: ['html'] },
30+
{ lang: 'YAML', script: 'test/src-coverage-run.ts', args: ['yaml'] },
2931
];
30-
// The 4 TS-family scope-gap adapters all read ONE shared env var (MONOGRAM_OFFICIAL_TM) for
31-
// the official grammar, so each needs its OWN grammar mapped in (CI sets MONOGRAM_OFFICIAL_TS/
32-
// TSX/JS/JSX). html/yaml read their own var (MONOGRAM_OFFICIAL_HTML/_YAML), inherited as-is;
33-
// vue is vendored. Absent (local, no env) → each adapter's VS Code-install fallback path.
32+
// The 4 TS-family scope-gap entries all read ONE shared env var (MONOGRAM_OFFICIAL_TM) for the
33+
// official grammar, so each needs its OWN grammar mapped in (CI sets MONOGRAM_OFFICIAL_TS/TSX/JS/JSX).
34+
// html/yaml read their own var (MONOGRAM_OFFICIAL_HTML/_YAML), inherited as-is; vue is vendored.
35+
// Absent (local, no env) → the driver's VS Code-install fallback path.
3436
const GAP = [
35-
{ lang: 'TypeScript', script: 'test/scope-gap-ts.ts', args: ['800'], officialEnv: 'MONOGRAM_OFFICIAL_TS' },
36-
{ lang: 'JavaScript', script: 'test/scope-gap-js.ts', args: ['800'], officialEnv: 'MONOGRAM_OFFICIAL_JS' },
37-
{ lang: 'JSX', script: 'test/scope-gap-jsx.ts', args: [], officialEnv: 'MONOGRAM_OFFICIAL_JSX' },
38-
{ lang: 'TSX', script: 'test/scope-gap-tsx.ts', args: [], officialEnv: 'MONOGRAM_OFFICIAL_TSX' },
39-
{ lang: 'HTML', script: 'test/scope-gap-html.ts', args: [] },
40-
{ lang: 'YAML', script: 'test/scope-gap-yaml.ts', args: [] },
41-
{ lang: 'Vue', script: 'test/scope-gap-vue.ts', args: [] },
37+
{ lang: 'TypeScript', script: 'test/scope-gap-run.ts', args: ['ts', '800'], officialEnv: 'MONOGRAM_OFFICIAL_TS' },
38+
{ lang: 'JavaScript', script: 'test/scope-gap-run.ts', args: ['js', '800'], officialEnv: 'MONOGRAM_OFFICIAL_JS' },
39+
{ lang: 'JSX', script: 'test/scope-gap-run.ts', args: ['jsx'], officialEnv: 'MONOGRAM_OFFICIAL_JSX' },
40+
{ lang: 'TSX', script: 'test/scope-gap-run.ts', args: ['tsx'], officialEnv: 'MONOGRAM_OFFICIAL_TSX' },
41+
{ lang: 'HTML', script: 'test/scope-gap-run.ts', args: ['html'] },
42+
{ lang: 'YAML', script: 'test/scope-gap-run.ts', args: ['yaml'] },
43+
{ lang: 'Vue', script: 'test/scope-gap-run.ts', args: ['vue'] },
4244
] as { lang: string; script: string; args: string[]; officialEnv?: string }[];
4345

4446
const pct = (v: number | null | undefined) => (v == null ? '—' : v.toFixed(1) + '%');
Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
1-
// scope-gap-jsx.ts — JSX (.jsx) adapter for the unified scope-gap harness. Grades VS Code's
2-
// OFFICIAL JavaScriptReact.tmLanguage.json AND Monogram's javascriptreact.tmLanguage.json against
3-
// the parser oracle (oracle.ts with ScriptKind.JSX). Both grammars declare scopeName `source.js.jsx`.
4-
// Neither the TS suite nor Test262 ships a .jsx corpus, so this uses a CURATED set exercising both
5-
// halves (plain JS + JSX), copied verbatim from src-coverage-jsx.ts. It is small, so token counts
6-
// are low; a real .jsx corpus is a follow-up. Run (bare node): node test/scope-gap-jsx.ts
7-
import ts from 'typescript';
8-
import { run } from './scope-gap.ts';
9-
import { oracle } from './oracle.ts';
1+
// curated-corpora.ts — small hand-written corpora shared by the folded scope-gap / src-coverage drivers.
2+
// JSX (plain-JS + JSX halves) and realistic HTML — the languages with no public single-file corpus.
103

11-
const OFFICIAL = process.env.MONOGRAM_OFFICIAL_TM
12-
?? '/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json';
13-
14-
// No TS types — these are .jsx (JavaScript + JSX) only. Copied verbatim from src-coverage-jsx.ts.
15-
const JSX_CASES: string[] = [
4+
export const JSX_CASES: string[] = [
165
// --- plain JS half ---
176
'const x = 1, y = 2;',
187
'function f(a, b = 1, ...rest) { return a + b + rest.length; }',
@@ -52,15 +41,25 @@ const JSX_CASES: string[] = [
5241
'const boolAttr = <button autofocus formNoValidate>ok</button>;',
5342
];
5443

55-
await run({
56-
name: 'JavaScriptReact (.jsx)',
57-
scopeName: 'source.js.jsx',
58-
officialPath: OFFICIAL,
59-
monogramPath: 'javascriptreact.tmLanguage.json',
60-
loadCorpus: () => JSX_CASES.map((code, i) => ({ name: `<curated #${i}>`, text: code })),
61-
roleOracle: (text) => oracle(text, ts.ScriptKind.JSX),
62-
isGradable: (text) => {
63-
const sf = ts.createSourceFile('c.jsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JSX);
64-
return (((sf as any).parseDiagnostics?.length ?? 0) === 0);
65-
},
66-
});
44+
export const HTML_GENERAL: string[] = [
45+
'<div class="container" id="main"><p>Hello <a href="/x">world</a>.</p></div>',
46+
'<ul><li>one</li><li>two</li><li>three</li></ul>',
47+
'<img src="a.png" alt="a picture" width="100" height="80">',
48+
'<input type="text" name="q" placeholder="Search" disabled>',
49+
'<button type="submit" class="btn btn-primary" data-id="42">Go</button>',
50+
'<section><h1>Title</h1><p>Body with <strong>bold</strong> and <em>italic</em>.</p></section>',
51+
'<nav><a href="/">Home</a> | <a href="/about">About</a></nav>',
52+
'<form action="/submit" method="post"><label for="n">Name</label><input id="n"></form>',
53+
'<table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>',
54+
'<!-- a comment --><div><!-- inline --><span>x</span></div>',
55+
'<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">',
56+
'<br><hr>',
57+
'<select><option value="1">One</option><option value="2" selected>Two</option></select>',
58+
'<video controls width="320"><source src="m.mp4" type="video/mp4"></video>',
59+
'<article data-index=3 hidden><header>H</header><footer>F</footer></article>',
60+
'<span class="a b c" title="x y z">text</span>',
61+
'<div\n class="multi-line"\n id="tag"\n data-x="1">body</div>',
62+
'<a href="https://example.com?a=1&b=2" target="_blank" rel="noopener">link</a>',
63+
'<label>Email <input type="email" required></label>',
64+
'<figure><img src="p.jpg" alt="photo"><figcaption>cap</figcaption></figure>',
65+
];

0 commit comments

Comments
 (0)