Skip to content

Commit 8d762ce

Browse files
committed
Generate per-arm CST destructurers; totality-gate them; consume them
src/gen-cst-match.ts is the VALUE-level sibling of gen-ast-types: for every rule it emits a typed result union ({ arm: 'if_', expr, stmt, stmt2? } | ...) and a match<Rule>(node, src) that re-derives which grammar alternative a node matched and binds its children to named fields - the discrimination the parser performed and the CST does not record. Each alternative compiles to a step plan (lit / litAlt-capture / tok / node / opt / many / sep / branches) that renders both the type and a cursor-based unifier; the unifier mirrors the engine's matcher semantics exactly: tokenType-exact literal checks (the $keyword-vs-Ident tie facts), the interpolated-template dual (a template token ref accepts a Template leaf OR a '$template' node), pratt operator forms (binaryOp/prefixOp/postfixOp synthesized beside led/nud arms), sep()'s consumed trailing delimiter, and greedy no-backtracking quantifiers (children always reflect the greedy success path, so local greedy decisions reproduce the parse). Pure-literal alternations capture the matched text as a string-literal-union field (alt('let','const','var') becomes let_Kw: 'let' | 'const' | 'var'). Wired into the gen pipeline: npm run gen emits <grammar>.cst-match.ts for all seven grammars. New gate test/cst-match-totality.ts (in check.ts, 29 gates): every node of every generated-corpus CST plus a TS conformance stride sample must destructure through its rule's matcher with full child consumption - 32,336 nodes, 0 misses on first run. ts-ast-lowering's statement layer now CONSUMES the generated matcher (switch on m.arm replaces the hand-probing that PAIN 3/5/7 documented), and picked up break/continue labels and with-statements in the rewrite; ts-ast-structure stays node-identical to tsc (32/32). Cost today (maximal workload - destructuring EVERY node, naive ordered arm try): +64% over the CST pass; the full tokens->CST->AST pipeline measures 0.96x vs tsc createSourceFile on the four bench files. Known v2 lever: the dispatcher tries arms in declaration order, so late arms (pratt binaryOp) pay ~20 failed tries - a first-child discriminator switch will cut most nodes to 1-2 tries.
1 parent b814d96 commit 8d762ce

12 files changed

Lines changed: 32124 additions & 112 deletions

html.cst-match.ts

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
// GENERATED by src/gen-cst-match.ts — do not edit. Per-arm CST destructurers for "html".
2+
/* eslint-disable */
3+
import type { AttrNode, CstChild, CstLeaf, DocumentNode, ElementNode, NodeNode } from "./html.cst-types.ts";
4+
5+
const isLit = (c: readonly CstChild[], i: number, src: string, text: string, tt: string): boolean => {
6+
const k = c[i] as CstLeaf | undefined;
7+
return k !== undefined && k.tokenType === tt && k.end - k.offset === text.length && src.startsWith(text, k.offset);
8+
};
9+
const isTok = (c: readonly CstChild[], i: number, name: string): boolean => {
10+
const k = c[i] as CstLeaf | undefined;
11+
return k !== undefined && k.tokenType === name;
12+
};
13+
const isNodeOf = (c: readonly CstChild[], i: number, rule: string): boolean => {
14+
const k = c[i] as { rule?: string } | undefined;
15+
return k !== undefined && k.rule === rule;
16+
};
17+
18+
export type ElementMatch =
19+
| { arm: "lt"; voidName: CstLeaf; attr: (AttrNode)[] }
20+
| { arm: "lt2"; name: CstLeaf; attr: (AttrNode)[] }
21+
| { arm: "lt3"; name: CstLeaf; attr: (AttrNode)[]; node: (NodeNode)[]; name2: CstLeaf }
22+
| { arm: "lt4"; name: CstLeaf; attr: (AttrNode)[]; rawText: CstLeaf };
23+
24+
function _Element$lt(c: readonly CstChild[], src: string): ElementMatch | null {
25+
let voidName: (CstLeaf) | undefined;
26+
const attr: (AttrNode)[] = [];
27+
let i = 0;
28+
if (!isLit(c, i, src, "<", "$punct")) return null;
29+
i++;
30+
if (!(isTok(c, i, "VoidName"))) return null;
31+
voidName = c[i] as CstLeaf;
32+
i++;
33+
for (;;) {
34+
const _t0 = i; let _t1 = true;
35+
_b2: {
36+
if (!isNodeOf(c, i, "Attr")) { _t1 = false; break _b2; }
37+
attr.push(c[i] as AttrNode);
38+
i++;
39+
}
40+
if (!_t1) { i = _t0; break; }
41+
if (i === _t0) break;
42+
}
43+
{
44+
const _t3 = i; let _t4 = true;
45+
_b5: {
46+
if (!isLit(c, i, src, "/", "$punct")) { _t4 = false; break _b5; }
47+
i++;
48+
}
49+
if (!_t4) i = _t3;
50+
}
51+
if (!isLit(c, i, src, ">", "$punct")) return null;
52+
i++;
53+
if (i !== c.length) return null;
54+
return { arm: "lt", voidName: voidName!, attr };
55+
}
56+
57+
function _Element$lt2(c: readonly CstChild[], src: string): ElementMatch | null {
58+
let name: (CstLeaf) | undefined;
59+
const attr: (AttrNode)[] = [];
60+
let i = 0;
61+
if (!isLit(c, i, src, "<", "$punct")) return null;
62+
i++;
63+
if (!(isTok(c, i, "Name"))) return null;
64+
name = c[i] as CstLeaf;
65+
i++;
66+
for (;;) {
67+
const _t0 = i; let _t1 = true;
68+
_b2: {
69+
if (!isNodeOf(c, i, "Attr")) { _t1 = false; break _b2; }
70+
attr.push(c[i] as AttrNode);
71+
i++;
72+
}
73+
if (!_t1) { i = _t0; break; }
74+
if (i === _t0) break;
75+
}
76+
if (!isLit(c, i, src, "/", "$punct")) return null;
77+
i++;
78+
if (!isLit(c, i, src, ">", "$punct")) return null;
79+
i++;
80+
if (i !== c.length) return null;
81+
return { arm: "lt2", name: name!, attr };
82+
}
83+
84+
function _Element$lt3(c: readonly CstChild[], src: string): ElementMatch | null {
85+
let name: (CstLeaf) | undefined;
86+
const attr: (AttrNode)[] = [];
87+
const node: (NodeNode)[] = [];
88+
let name2: (CstLeaf) | undefined;
89+
let i = 0;
90+
if (!isLit(c, i, src, "<", "$punct")) return null;
91+
i++;
92+
if (!(isTok(c, i, "Name"))) return null;
93+
name = c[i] as CstLeaf;
94+
i++;
95+
for (;;) {
96+
const _t0 = i; let _t1 = true;
97+
_b2: {
98+
if (!isNodeOf(c, i, "Attr")) { _t1 = false; break _b2; }
99+
attr.push(c[i] as AttrNode);
100+
i++;
101+
}
102+
if (!_t1) { i = _t0; break; }
103+
if (i === _t0) break;
104+
}
105+
if (!isLit(c, i, src, ">", "$punct")) return null;
106+
i++;
107+
for (;;) {
108+
const _t3 = i; let _t4 = true;
109+
_b5: {
110+
if (!isNodeOf(c, i, "Node")) { _t4 = false; break _b5; }
111+
node.push(c[i] as NodeNode);
112+
i++;
113+
}
114+
if (!_t4) { i = _t3; break; }
115+
if (i === _t3) break;
116+
}
117+
if (!isLit(c, i, src, "<", "$punct")) return null;
118+
i++;
119+
if (!isLit(c, i, src, "/", "$punct")) return null;
120+
i++;
121+
if (!(isTok(c, i, "Name"))) return null;
122+
name2 = c[i] as CstLeaf;
123+
i++;
124+
if (!isLit(c, i, src, ">", "$punct")) return null;
125+
i++;
126+
if (i !== c.length) return null;
127+
return { arm: "lt3", name: name!, attr, node, name2: name2! };
128+
}
129+
130+
function _Element$lt4(c: readonly CstChild[], src: string): ElementMatch | null {
131+
let name: (CstLeaf) | undefined;
132+
const attr: (AttrNode)[] = [];
133+
let rawText: (CstLeaf) | undefined;
134+
let i = 0;
135+
if (!isLit(c, i, src, "<", "$punct")) return null;
136+
i++;
137+
if (!(isTok(c, i, "Name"))) return null;
138+
name = c[i] as CstLeaf;
139+
i++;
140+
for (;;) {
141+
const _t0 = i; let _t1 = true;
142+
_b2: {
143+
if (!isNodeOf(c, i, "Attr")) { _t1 = false; break _b2; }
144+
attr.push(c[i] as AttrNode);
145+
i++;
146+
}
147+
if (!_t1) { i = _t0; break; }
148+
if (i === _t0) break;
149+
}
150+
if (!isLit(c, i, src, ">", "$punct")) return null;
151+
i++;
152+
if (!(isTok(c, i, "RawText"))) return null;
153+
rawText = c[i] as CstLeaf;
154+
i++;
155+
if (i !== c.length) return null;
156+
return { arm: "lt4", name: name!, attr, rawText: rawText! };
157+
}
158+
159+
export function matchElement(n: ElementNode, src: string): ElementMatch {
160+
const c = n.children;
161+
{ const m = _Element$lt(c, src); if (m !== null) return m; }
162+
{ const m = _Element$lt2(c, src); if (m !== null) return m; }
163+
{ const m = _Element$lt3(c, src); if (m !== null) return m; }
164+
{ const m = _Element$lt4(c, src); if (m !== null) return m; }
165+
throw new Error("matchElement: no arm matches" + ' @' + n.offset);
166+
}
167+
168+
export type AttrMatch =
169+
| { arm: "name"; name: CstLeaf; attrValue?: CstLeaf; name2?: CstLeaf; unquotedValue?: CstLeaf };
170+
171+
function _Attr$name(c: readonly CstChild[], src: string): AttrMatch | null {
172+
let name: (CstLeaf) | undefined;
173+
let attrValue: (CstLeaf) | undefined;
174+
let name2: (CstLeaf) | undefined;
175+
let unquotedValue: (CstLeaf) | undefined;
176+
let i = 0;
177+
if (!(isTok(c, i, "Name"))) return null;
178+
name = c[i] as CstLeaf;
179+
i++;
180+
{
181+
const _t0 = i; let _t1 = true;
182+
_b2: {
183+
if (!isLit(c, i, src, "=", "$punct")) { _t1 = false; break _b2; }
184+
i++;
185+
{
186+
let _t3 = false;
187+
if (!_t3) {
188+
const _t4 = i; let _t5 = true;
189+
_b6: {
190+
if (!(isTok(c, i, "AttrValue"))) { _t5 = false; break _b6; }
191+
attrValue = c[i] as CstLeaf;
192+
i++;
193+
}
194+
if (_t5) _t3 = true; else i = _t4;
195+
}
196+
if (!_t3) {
197+
const _t7 = i; let _t8 = true;
198+
_b9: {
199+
if (!(isTok(c, i, "Name"))) { _t8 = false; break _b9; }
200+
name2 = c[i] as CstLeaf;
201+
i++;
202+
}
203+
if (_t8) _t3 = true; else i = _t7;
204+
}
205+
if (!_t3) {
206+
const _t10 = i; let _t11 = true;
207+
_b12: {
208+
if (!(isTok(c, i, "UnquotedValue"))) { _t11 = false; break _b12; }
209+
unquotedValue = c[i] as CstLeaf;
210+
i++;
211+
}
212+
if (_t11) _t3 = true; else i = _t10;
213+
}
214+
if (!_t3) { _t1 = false; break _b2; }
215+
}
216+
}
217+
if (!_t1) i = _t0;
218+
}
219+
if (i !== c.length) return null;
220+
return { arm: "name", name: name!, attrValue, name2, unquotedValue };
221+
}
222+
223+
export function matchAttr(n: AttrNode, src: string): AttrMatch {
224+
const c = n.children;
225+
{ const m = _Attr$name(c, src); if (m !== null) return m; }
226+
throw new Error("matchAttr: no arm matches" + ' @' + n.offset);
227+
}
228+
229+
export type NodeMatch =
230+
| { arm: "element"; element: ElementNode }
231+
| { arm: "comment"; comment: CstLeaf }
232+
| { arm: "rawText"; rawText: CstLeaf }
233+
| { arm: "text"; text: CstLeaf };
234+
235+
function _Node$element(c: readonly CstChild[], src: string): NodeMatch | null {
236+
let element: (ElementNode) | undefined;
237+
let i = 0;
238+
if (!isNodeOf(c, i, "Element")) return null;
239+
element = c[i] as ElementNode;
240+
i++;
241+
if (i !== c.length) return null;
242+
return { arm: "element", element: element! };
243+
}
244+
245+
function _Node$comment(c: readonly CstChild[], src: string): NodeMatch | null {
246+
let comment: (CstLeaf) | undefined;
247+
let i = 0;
248+
if (!(isTok(c, i, "Comment"))) return null;
249+
comment = c[i] as CstLeaf;
250+
i++;
251+
if (i !== c.length) return null;
252+
return { arm: "comment", comment: comment! };
253+
}
254+
255+
function _Node$rawText(c: readonly CstChild[], src: string): NodeMatch | null {
256+
let rawText: (CstLeaf) | undefined;
257+
let i = 0;
258+
if (!(isTok(c, i, "RawText"))) return null;
259+
rawText = c[i] as CstLeaf;
260+
i++;
261+
if (i !== c.length) return null;
262+
return { arm: "rawText", rawText: rawText! };
263+
}
264+
265+
function _Node$text(c: readonly CstChild[], src: string): NodeMatch | null {
266+
let text: (CstLeaf) | undefined;
267+
let i = 0;
268+
if (!(isTok(c, i, "Text"))) return null;
269+
text = c[i] as CstLeaf;
270+
i++;
271+
if (i !== c.length) return null;
272+
return { arm: "text", text: text! };
273+
}
274+
275+
export function matchNode(n: NodeNode, src: string): NodeMatch {
276+
const c = n.children;
277+
{ const m = _Node$element(c, src); if (m !== null) return m; }
278+
{ const m = _Node$comment(c, src); if (m !== null) return m; }
279+
{ const m = _Node$rawText(c, src); if (m !== null) return m; }
280+
{ const m = _Node$text(c, src); if (m !== null) return m; }
281+
throw new Error("matchNode: no arm matches" + ' @' + n.offset);
282+
}
283+
284+
export type DocumentMatch =
285+
| { arm: "element"; element: (ElementNode)[]; comment: (CstLeaf)[]; text: (CstLeaf)[] };
286+
287+
function _Document$element(c: readonly CstChild[], src: string): DocumentMatch | null {
288+
const element: (ElementNode)[] = [];
289+
const comment: (CstLeaf)[] = [];
290+
const text: (CstLeaf)[] = [];
291+
let i = 0;
292+
for (;;) {
293+
const _t0 = i; let _t1 = true;
294+
_b2: {
295+
{
296+
let _t3 = false;
297+
if (!_t3) {
298+
const _t4 = i; let _t5 = true;
299+
_b6: {
300+
if (!isNodeOf(c, i, "Element")) { _t5 = false; break _b6; }
301+
element.push(c[i] as ElementNode);
302+
i++;
303+
}
304+
if (_t5) _t3 = true; else i = _t4;
305+
}
306+
if (!_t3) {
307+
const _t7 = i; let _t8 = true;
308+
_b9: {
309+
if (!(isTok(c, i, "Comment"))) { _t8 = false; break _b9; }
310+
comment.push(c[i] as CstLeaf);
311+
i++;
312+
}
313+
if (_t8) _t3 = true; else i = _t7;
314+
}
315+
if (!_t3) {
316+
const _t10 = i; let _t11 = true;
317+
_b12: {
318+
if (!(isTok(c, i, "Text"))) { _t11 = false; break _b12; }
319+
text.push(c[i] as CstLeaf);
320+
i++;
321+
}
322+
if (_t11) _t3 = true; else i = _t10;
323+
}
324+
if (!_t3) { _t1 = false; break _b2; }
325+
}
326+
}
327+
if (!_t1) { i = _t0; break; }
328+
if (i === _t0) break;
329+
}
330+
if (i !== c.length) return null;
331+
return { arm: "element", element, comment, text };
332+
}
333+
334+
export function matchDocument(n: DocumentNode, src: string): DocumentMatch {
335+
const c = n.children;
336+
{ const m = _Document$element(c, src); if (m !== null) return m; }
337+
throw new Error("matchDocument: no arm matches" + ' @' + n.offset);
338+
}
339+
340+
/** rule name → its matcher (generic walking; the totality gate uses this). */
341+
export const MATCHERS: Record<string, (n: never, src: string) => { arm: string }> = {
342+
"Element": matchElement,
343+
"Attr": matchAttr,
344+
"Node": matchNode,
345+
"Document": matchDocument,
346+
};

0 commit comments

Comments
 (0)