Skip to content

Commit ebeeff6

Browse files
committed
CST leaves are span-only: drop the text field from the contract
A leaf is now {kind, tokenType, offset, end}. Its text was redundant data — text === source.slice(offset, end) held everywhere once the yaml flow-merge anomaly was fixed (previous commit), and the cst-text-invariant gate now pins the span-only shape itself (no text property, sane spans; 43,999 leaves across all seven grammars + a TS corpus sample). Consumers derive text from the source they parsed: both engines export getText(node, source), and the in-repo consumers (html-conformance tree extraction, the generative net's leafRoles, gap-ledger probes) take the input alongside the CST. Both engines change together: matchers and the pratt operator paths stop materializing text (matchKwLit/matchPuLit lose their value parameter — the leaf no longer needs it), the no-unary-LHS head check and the markup open/close tag-name comparisons slice the source span, and the generated *.cst-types.ts leaf interface drops the field. interp ≡ emit holds on the full 18,805-file corpus; lexer streams and reject messages identical; 27/27 gates. Side effect on the PR#4 bench: dropping ~10k leaf-text slices per parse and shrinking every leaf by a field measures +2.1..+14.7% aggregate, 6/8 runs positive (mean ~+7%).
1 parent 54d811e commit ebeeff6

16 files changed

Lines changed: 77 additions & 76 deletions

html.cst-types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export type TokenType =
3232
export interface CstLeaf extends CstPos {
3333
kind: 'leaf';
3434
tokenType: TokenType;
35-
text: string;
3635
}
3736

3837
/** `Element` node. Children (flattened, in source order) are drawn from: */

javascript.cst-types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export type TokenType =
4141
export interface CstLeaf extends CstPos {
4242
kind: 'leaf';
4343
tokenType: TokenType;
44-
text: string;
4544
}
4645

4746
/** Synthetic node the parser builds for an interpolated template literal. */

javascriptreact.cst-types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export type TokenType =
4343
export interface CstLeaf extends CstPos {
4444
kind: 'leaf';
4545
tokenType: TokenType;
46-
text: string;
4746
}
4847

4948
/** Synthetic node the parser builds for an interpolated template literal. */

src/emit-parser.ts

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,8 +1127,8 @@ class Emitter {
11271127
// with the value's baked int (so the runtime does int compares, not string work).
11281128
matchLiteralCall(value: string): string {
11291129
const d = this.a.symtab.classifyKey(value);
1130-
if (d.kind === 'kw') return `matchKwLit(${J(value)}, ${d.t})`;
1131-
if (d.kind === 'punct') return value === '>' ? `matchPuLitGT(${d.t})` : `matchPuLit(${J(value)}, ${d.t})`;
1130+
if (d.kind === 'kw') return `matchKwLit(${d.t})`;
1131+
if (d.kind === 'punct') return value === '>' ? `matchPuLitGT(${d.t})` : `matchPuLit(${d.t})`;
11321132
// A literal key that classifies as a token-name (a token name used as a literal):
11331133
// unreachable for real grammars, but stay safe via the generic matchLiteral.
11341134
return `matchLiteral(${J(value)})`;
@@ -1288,8 +1288,6 @@ function resolveLexerImport(): string { return pathResolve(__dir, 'gen-lexer.ts'
12881288
// ONLY change: where the interpreter called matchExpr(alt)/matchSeq(items) per arm,
12891289
// these call the GENERATED per-arm matcher functions (installed via the rule fns).
12901290
function emitRuntime(e: Emitter) {
1291-
// Per-site text materialization (soa: slice the source span; fallback: text column).
1292-
const TXT_OE = e.soa ? 'src.slice(off, end)' : 'tkText[pos]';
12931291
// Column element type: Uint8 when the kind/literal id spaces fit a byte.
12941292
const st = e.a.symtab;
12951293
let tMax = 1;
@@ -1346,34 +1344,34 @@ function childEnd(c) { return c.end; }
13461344
// && tok.text === value. With interned kinds that is tok.k >= K_NAMED_MIN (a declared
13471345
// token name; '' is PUNCT, templates are below NAMED_MIN) && tok.t === KW(value).
13481346
// Returns the SAME $keyword leaf as before. value/kw are baked by the caller.
1349-
function matchKwLit(value, kw) {
1347+
function matchKwLit(kw) {
13501348
// A kw-range t can only come from a named token (template spans never intern to a
13511349
// keyword), so the old k >= K_NAMED_MIN guard was redundant — one int compare.
13521350
if (pos >= cap || tkT[pos] !== kw) return null;
13531351
const off = tkOff[pos];
13541352
const end = tkEnd[pos];
13551353
if (++pos > maxPos) maxPos = pos;
1356-
return { kind: 'leaf', tokenType: '$keyword', text: value, offset: off, end };
1354+
return { kind: 'leaf', tokenType: '$keyword', offset: off, end };
13571355
}
13581356
// Punct literal: tok.type === '' && tok.text === value, with the gt-splice fallback.
13591357
// tok.t === PU(value) is the exact-text fast path; the splice handles a longer
13601358
// gt-led token matching the gt key. value/pu are baked by the caller.
1361-
function matchPuLit(value, pu) {
1359+
function matchPuLit(pu) {
13621360
// A pu-range t can only come from a punct token, so the old k === K_PUNCT guard was
13631361
// redundant — one int compare. The '>'-split lives only in matchPuLitGT ('>' sites).
13641362
if (pos >= cap || tkT[pos] !== pu) return null;
13651363
const off = tkOff[pos];
13661364
const end = tkEnd[pos];
13671365
if (++pos > maxPos) maxPos = pos;
1368-
return { kind: 'leaf', tokenType: '$punct', text: value, offset: off, end };
1366+
return { kind: 'leaf', tokenType: '$punct', offset: off, end };
13691367
}
13701368
function matchPuLitGT(pu) {
13711369
if (pos >= cap) return null;
13721370
const off = tkOff[pos];
13731371
if (tkT[pos] === pu) {
13741372
const end = tkEnd[pos];
13751373
if (++pos > maxPos) maxPos = pos;
1376-
return { kind: 'leaf', tokenType: '$punct', text: '>', offset: off, end };
1374+
return { kind: 'leaf', tokenType: '$punct', offset: off, end };
13771375
}
13781376
// Split multi-'>' tokens: '>>', '>>>', '>>=', '>>>=' can yield a single '>': shift the
13791377
// columns up one slot and write the '>' + rest pair in place (both born flag-less,
@@ -1397,17 +1395,17 @@ function matchPuLitGT(pu) {
13971395
memoNode.fill(undefined);
13981396
memoEnd.fill(undefined);
13991397
if (++pos > maxPos) maxPos = pos;
1400-
return { kind: 'leaf', tokenType: '$punct', text: '>', offset: off, end: off + 1 };
1398+
return { kind: 'leaf', tokenType: '$punct', offset: off, end: off + 1 };
14011399
}
14021400
return null;
14031401
}
14041402
// Generic matchLiteral kept for any unspecialized site: classify value via the baked
14051403
// tables (no per-call isKeywordLiteral / string compares) and delegate.
14061404
function matchLiteral(value) {
14071405
const kw = LIT_KW.get(value);
1408-
if (kw !== undefined) return matchKwLit(value, kw);
1406+
if (kw !== undefined) return matchKwLit(kw);
14091407
if (value === '>') return matchPuLitGT(LIT_PU.get(value) ?? 0);
1410-
return matchPuLit(value, LIT_PU.get(value) ?? 0);
1408+
return matchPuLit(LIT_PU.get(value) ?? 0);
14111409
}
14121410
14131411
// Match a token ref by its baked TYPE kind: tok.type === name ⟺ tok.k === nameKind.
@@ -1416,9 +1414,8 @@ function matchTokK(name, nameKind) {
14161414
if (pos >= cap || tkK[pos] !== nameKind) return null;
14171415
const off = tkOff[pos];
14181416
const end = tkEnd[pos];
1419-
const text = ${TXT_OE};
14201417
if (++pos > maxPos) maxPos = pos;
1421-
return { kind: 'leaf', tokenType: name, text, offset: off, end };
1418+
return { kind: 'leaf', tokenType: name, offset: off, end };
14221419
}
14231420
14241421
// (First-token / FIRST-set gating is baked at emit time: per-set _qN byte-table fns
@@ -1428,31 +1425,31 @@ function parseTemplateExpr() {
14281425
if (pos >= cap) return null;
14291426
const k = tkK[pos];
14301427
if (k === K_TPL_TOKEN) {
1431-
const off = tkOff[pos]; const end = tkEnd[pos]; const text = ${TXT_OE};
1428+
const off = tkOff[pos]; const end = tkEnd[pos];
14321429
if (++pos > maxPos) maxPos = pos;
1433-
return { kind: 'leaf', tokenType: templateTokenName, text, offset: off, end };
1430+
return { kind: 'leaf', tokenType: templateTokenName, offset: off, end };
14341431
}
14351432
if (k === K_TEMPLATE_HEAD) {
14361433
const children = [];
1437-
{ const off = tkOff[pos]; const end = tkEnd[pos]; const text = ${TXT_OE};
1434+
{ const off = tkOff[pos]; const end = tkEnd[pos];
14381435
if (++pos > maxPos) maxPos = pos;
1439-
children.push({ kind: 'leaf', tokenType: '$templateHead', text, offset: off, end }); }
1436+
children.push({ kind: 'leaf', tokenType: '$templateHead', offset: off, end }); }
14401437
const interpRule = currentPrattContext ?? EXPR_RULE;
14411438
while (true) {
14421439
const exprNode = RULES[interpRule]();
14431440
if (exprNode) children.push(exprNode);
14441441
if (pos >= cap) break;
14451442
const nk = tkK[pos];
14461443
if (nk === K_TEMPLATE_MIDDLE) {
1447-
const off = tkOff[pos]; const end = tkEnd[pos]; const text = ${TXT_OE};
1444+
const off = tkOff[pos]; const end = tkEnd[pos];
14481445
if (++pos > maxPos) maxPos = pos;
1449-
children.push({ kind: 'leaf', tokenType: '$templateMiddle', text, offset: off, end });
1446+
children.push({ kind: 'leaf', tokenType: '$templateMiddle', offset: off, end });
14501447
continue;
14511448
}
14521449
if (nk === K_TEMPLATE_TAIL) {
1453-
const off = tkOff[pos]; const end = tkEnd[pos]; const text = ${TXT_OE};
1450+
const off = tkOff[pos]; const end = tkEnd[pos];
14541451
if (++pos > maxPos) maxPos = pos;
1455-
children.push({ kind: 'leaf', tokenType: '$templateTail', text, offset: off, end });
1452+
children.push({ kind: 'leaf', tokenType: '$templateTail', offset: off, end });
14561453
break;
14571454
}
14581455
break;
@@ -1589,9 +1586,9 @@ function emitPrattRule(e: Emitter, a: ReturnType<typeof analyze>, rule: RuleDecl
15891586
e.emit(` if (pos < cap) {`);
15901587
e.emit(` const info = PREFIX_BY_T[tkT[pos]];`);
15911588
e.emit(` if (info) {`);
1592-
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos]; const _tx = ${e.textAt('pos')};`);
1589+
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos];`);
15931590
e.emit(` if (++pos > maxPos) maxPos = pos;`);
1594-
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', text: _tx, offset: _o, end: _e };`);
1591+
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', offset: _o, end: _e };`);
15951592
e.emit(` const rhs = ${ruleFn}_pratt(info.rbp);`);
15961593
e.emit(` if (rhs && pos > bestNudPos) { lhs = { kind: 'node', rule: ${J(rule.name)}, children: [opLeaf, rhs], offset: opLeaf.offset, end: rhs.end }; bestNudPos = pos; }`);
15971594
e.emit(` }`);
@@ -1656,20 +1653,20 @@ function emitPrattRule(e: Emitter, a: ReturnType<typeof analyze>, rule: RuleDecl
16561653
e.emit(` if (info && info.lbp > minBp) {`);
16571654
e.emit(` if (info.position === 'postfix') {`);
16581655
e.emit(` if (!tailClosed) {`);
1659-
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos]; const _tx = ${e.textAt('pos')};`);
1656+
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos];`);
16601657
e.emit(` if (++pos > maxPos) maxPos = pos;`);
1661-
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', text: _tx, offset: _o, end: _e };`);
1658+
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', offset: _o, end: _e };`);
16621659
e.emit(` lhs = { kind: 'node', rule: ${J(rule.name)}, children: [lhs, opLeaf], offset: lhs.offset, end: opLeaf.end };`);
16631660
e.emit(` tailClosed = true; matched = true;`);
16641661
e.emit(` }`);
16651662
e.emit(` } else {`);
16661663
e.emit(` if (NOUNARY_T[tkT[pos]] !== 0 && lhs.kind === 'node') {`);
16671664
e.emit(` const head = lhs.children[0];`);
1668-
e.emit(` if (head && head.kind === 'leaf' && head.tokenType === '$operator' && prefixOps.has(head.text) && !postfixOpValues.has(head.text)) { return null; }`);
1665+
e.emit(` if (head && head.kind === 'leaf' && head.tokenType === '$operator' && prefixOps.has(src.slice(head.offset, head.end)) && !postfixOpValues.has(src.slice(head.offset, head.end))) { return null; }`);
16691666
e.emit(` }`);
1670-
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos]; const _tx = ${e.textAt('pos')};`);
1667+
e.emit(` const _o = tkOff[pos]; const _e = tkEnd[pos];`);
16711668
e.emit(` if (++pos > maxPos) maxPos = pos;`);
1672-
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', text: _tx, offset: _o, end: _e };`);
1669+
e.emit(` const opLeaf = { kind: 'leaf', tokenType: '$operator', offset: _o, end: _e };`);
16731670
e.emit(` const rhs = ${ruleFn}_pratt(info.rbp);`);
16741671
e.emit(` if (rhs) { lhs = { kind: 'node', rule: ${J(rule.name)}, children: [lhs, opLeaf, rhs], offset: lhs.offset, end: rhs.end }; matched = true; }`);
16751672
e.emit(` else { pos = ledSaved; }`);
@@ -1829,6 +1826,11 @@ export function tokenAt(i) {
18291826
};
18301827
}
18311828
1829+
// The CST is span-only: a node's text is derived from the source it was parsed from.
1830+
export function getText(node, source) {
1831+
return source.slice(node.offset, node.end);
1832+
}
1833+
18321834
export function parse(source, entryRule) {
18331835
${e.soa ? ` tokenize(source);` : String.raw` src = source;
18341836
const _toks = tokenize(source);

src/gen-ast-types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ export function generateAstTypes(grammar: CstGrammar): string {
9797
lines.push('export interface CstLeaf extends CstPos {');
9898
lines.push(' kind: \'leaf\';');
9999
lines.push(' tokenType: TokenType;');
100-
lines.push(' text: string;');
101100
lines.push('}');
102101
lines.push('');
103102

src/gen-parser.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export interface CstNode {
1515
export interface CstLeaf {
1616
kind: 'leaf';
1717
tokenType: string;
18-
text: string;
1918
offset: number;
2019
end: number;
2120
}
@@ -33,6 +32,11 @@ interface OpInfo {
3332

3433
// ── Parser ──
3534

35+
// The CST is span-only: a node's text is derived from the source it was parsed from.
36+
export function getText(node: { offset: number; end: number }, source: string): string {
37+
return source.slice(node.offset, node.end);
38+
}
39+
3640
export function createParser(grammar: CstGrammar) {
3741
const tokenNames = new Set(grammar.tokens.map(t => t.name));
3842

@@ -760,14 +764,14 @@ export function createParser(grammar: CstGrammar) {
760764
// Keyword literal: match against Ident token with same text
761765
if (tok.type !== '' && tokenNames.has(tok.type) && tok.text === value) {
762766
pos++;
763-
return { kind: 'leaf', tokenType: '$keyword', text: value, offset: tok.offset, end: tok.offset + tok.text.length };
767+
return { kind: 'leaf', tokenType: '$keyword', offset: tok.offset, end: tok.offset + tok.text.length };
764768
}
765769
return null;
766770
}
767771
// Punctuation literal
768772
if (tok.type === '' && tok.text === value) {
769773
pos++;
770-
return { kind: 'leaf', tokenType: '$punct', text: value, offset: tok.offset, end: tok.offset + tok.text.length };
774+
return { kind: 'leaf', tokenType: '$punct', offset: tok.offset, end: tok.offset + tok.text.length };
771775
}
772776
// Split multi-`>` tokens: `>>`, `>>>`, `>>=`, `>>>=` can yield a single `>`
773777
if (value === '>' && tok.type === '' && tok.text.length > 1 && tok.text[0] === '>') {
@@ -778,7 +782,7 @@ export function createParser(grammar: CstGrammar) {
778782
);
779783
memo.clear(); // splice shifts later token indices → memo entries are stale
780784
pos++;
781-
return { kind: 'leaf', tokenType: '$punct', text: '>', offset: tok.offset, end: tok.offset + 1 };
785+
return { kind: 'leaf', tokenType: '$punct', offset: tok.offset, end: tok.offset + 1 };
782786
}
783787
return null;
784788
}
@@ -789,7 +793,7 @@ export function createParser(grammar: CstGrammar) {
789793
if (!tok) return null;
790794
if (tok.type === name) {
791795
pos++;
792-
return { kind: 'leaf', tokenType: name, text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length };
796+
return { kind: 'leaf', tokenType: name, offset: tok.offset, end: tok.offset + tok.text.length };
793797
}
794798
return null;
795799
}
@@ -808,12 +812,12 @@ export function createParser(grammar: CstGrammar) {
808812
if (!tok) return null;
809813
if (tok.type === templateTokenName) {
810814
pos++;
811-
return { kind: 'leaf', tokenType: templateTokenName, text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length };
815+
return { kind: 'leaf', tokenType: templateTokenName, offset: tok.offset, end: tok.offset + tok.text.length };
812816
}
813817
if (tok.type === '$templateHead') {
814818
const children: CstChild[] = [];
815819
pos++;
816-
children.push({ kind: 'leaf', tokenType: '$templateHead', text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length });
820+
children.push({ kind: 'leaf', tokenType: '$templateHead', offset: tok.offset, end: tok.offset + tok.text.length });
817821
const interpRule = currentPrattContext ?? findExprRule();
818822
while (true) {
819823
const exprNode = parseRule(interpRule);
@@ -822,12 +826,12 @@ export function createParser(grammar: CstGrammar) {
822826
if (!next) break;
823827
if (next.type === '$templateMiddle') {
824828
pos++;
825-
children.push({ kind: 'leaf', tokenType: '$templateMiddle', text: next.text, offset: next.offset, end: next.offset + next.text.length });
829+
children.push({ kind: 'leaf', tokenType: '$templateMiddle', offset: next.offset, end: next.offset + next.text.length });
826830
continue;
827831
}
828832
if (next.type === '$templateTail') {
829833
pos++;
830-
children.push({ kind: 'leaf', tokenType: '$templateTail', text: next.text, offset: next.offset, end: next.offset + next.text.length });
834+
children.push({ kind: 'leaf', tokenType: '$templateTail', offset: next.offset, end: next.offset + next.text.length });
831835
break;
832836
}
833837
break;
@@ -1008,7 +1012,7 @@ export function createParser(grammar: CstGrammar) {
10081012
const info = prefixOps.get(key);
10091013
if (info) {
10101014
pos++;
1011-
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length };
1015+
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', offset: tok.offset, end: tok.offset + tok.text.length };
10121016
const rhs = parsePratt(rule, info.rbp);
10131017
if (rhs && pos > bestNudPos) {
10141018
lhs = { kind: 'node', rule: rule.name, children: [opLeaf, rhs], offset: opLeaf.offset, end: rhs.end };
@@ -1086,7 +1090,7 @@ export function createParser(grammar: CstGrammar) {
10861090
if (info.position === 'postfix') {
10871091
if (!tailClosed) { // can't postfix an update expr (`a++ --`)
10881092
pos++;
1089-
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length };
1093+
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', offset: tok.offset, end: tok.offset + tok.text.length };
10901094
lhs = { kind: 'node', rule: rule.name, children: [lhs, opLeaf], offset: lhs.offset, end: opLeaf.end };
10911095
tailClosed = true;
10921096
matched = true;
@@ -1102,12 +1106,12 @@ export function createParser(grammar: CstGrammar) {
11021106
if (noUnaryLhsOps.has(tokKey) && lhs.kind === 'node') {
11031107
const head = lhs.children[0];
11041108
if (head?.kind === 'leaf' && head.tokenType === '$operator'
1105-
&& prefixOps.has(head.text) && !postfixOpValues.has(head.text)) {
1109+
&& prefixOps.has(source.slice(head.offset, head.end)) && !postfixOpValues.has(source.slice(head.offset, head.end))) {
11061110
return null;
11071111
}
11081112
}
11091113
pos++;
1110-
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', text: tok.text, offset: tok.offset, end: tok.offset + tok.text.length };
1114+
const opLeaf: CstLeaf = { kind: 'leaf', tokenType: '$operator', offset: tok.offset, end: tok.offset + tok.text.length };
11111115
const rhs = parsePratt(rule, info.rbp);
11121116
if (rhs) {
11131117
lhs = { kind: 'node', rule: rule.name, children: [lhs, opLeaf, rhs], offset: lhs.offset, end: rhs.end };
@@ -1243,7 +1247,7 @@ export function createParser(grammar: CstGrammar) {
12431247
if (result === null) { pos = saved; return null; }
12441248
// The open-tag name leaf (a name token) — drives the optional-end lookup.
12451249
if (item.type === 'ref' && c.nameTokens.has(item.name) && result[0]?.kind === 'leaf') {
1246-
openName = result[0].text;
1250+
openName = source.slice(result[0].offset, result[0].end);
12471251
}
12481252
children.push(...result);
12491253
}
@@ -1275,7 +1279,7 @@ export function createParser(grammar: CstGrammar) {
12751279
const close = matchSeq(c.items.slice(c.closeStart));
12761280
if (close !== null) {
12771281
const closeName = close.find((ch, i) => i > 0 && ch.kind === 'leaf' && c.nameTokens.has((ch as CstLeaf).tokenType)) as CstLeaf | undefined;
1278-
if (!closeName || closeName.text.toLowerCase() === openName.toLowerCase()) {
1282+
if (!closeName || source.slice(closeName.offset, closeName.end).toLowerCase() === openName.toLowerCase()) {
12791283
children.push(...close);
12801284
return children;
12811285
}

0 commit comments

Comments
 (0)