@@ -376,6 +376,28 @@ function predAltBody(branches: Step[][], firsts?: FirstSig[]): string {
376376 return `const t = peek(); if (t === null) return false; ${ arms } return false;` ;
377377}
378378
379+ /** Entry shape eligible for top-level subtree reuse: single alt whose sole step is
380+ * `star(rule)` or `star(alt(rule, rule, …))` (calc / javascript). */
381+ function topReusePlan ( ir : ParserIR ) : { topOneBody : string } | null {
382+ const entry = ir . rules . find ( ( r ) => r . name === ir . entry ) ;
383+ if ( ! entry || entry . kind !== 'rd' || entry . alts . length !== 1 ) return null ;
384+ const alt = entry . alts [ 0 ] ;
385+ if ( alt . length !== 1 || alt [ 0 ] . t !== 'star' ) return null ;
386+ const step = alt [ 0 ] . step ;
387+ if ( step . t === 'rule' ) return { topOneBody : ` return parse${ step . name } ();` } ;
388+ if ( step . t === 'alt' ) {
389+ for ( const br of step . branches ) {
390+ if ( br . length !== 1 || br [ 0 ] . t !== 'rule' ) return null ;
391+ }
392+ const tries = step . branches . map ( ( br ) => {
393+ const name = ( br [ 0 ] as { t : 'rule' ; name : string } ) . name ;
394+ return ` { const sp = pos; const n = parse${ name } (); if (n !== null) return n; pos = sp; }` ;
395+ } ) . join ( '\n' ) ;
396+ return { topOneBody : `${ tries } \n return null;` } ;
397+ }
398+ return null ;
399+ }
400+
379401function rdRule ( r : RdRule ) : string {
380402 if ( r . predictive ) {
381403 const arm = ( steps : Step [ ] , i : number ) => ` ${ i === 0 ? 'if' : 'else if' } (${ firstCond ( r . altFirst [ i ] , 't' ) } ) { const kids: Cst[] = []; if (${ steps . map ( stepCond ) . join ( ' && ' ) } ) return branch(${ J ( r . cstName ) } , kids, save); }` ;
@@ -396,6 +418,26 @@ ${r.alts.map(alt).join('\n')}
396418}` ;
397419}
398420
421+ /** Entry rule that records per-top-kid lookahead ext via parseTopOne. */
422+ function rdEntryWithReuse ( r : RdRule , plan : { topOneBody : string } ) : string {
423+ return `function parseTopOne(): Node | null {
424+ ${ plan . topOneBody }
425+ }
426+ function parse${ r . name } (): Node | null {
427+ const save = pos;
428+ const kids: Cst[] = [];
429+ for (;;) {
430+ const sp = pos;
431+ maxLook = 0;
432+ const n = parseTopOne();
433+ if (n === null) { pos = sp; break; }
434+ n.ext = Math.max(n.tokEnd, maxLook);
435+ kids.push(n);
436+ }
437+ return branch(${ J ( r . cstName ) } , kids, save);
438+ }` ;
439+ }
440+
399441function prattRule ( r : PrattRule , tpl : TplCfg | null ) : string {
400442 const tplNud = tpl && r . nudToks . includes ( tpl . token )
401443 ? ` if (t.kind === '$templateHead') { const node = matchTemplate(); return node === null ? null : { rule: ${ J ( r . cstName ) } , children: [node], offset: node.offset, end: node.end, tokStart: node.tokStart, tokEnd: node.tokEnd }; }\n`
@@ -488,6 +530,7 @@ function docEditBlock(ir: ParserIR): string {
488530 const rxOnly = ! ! ( ir . regexCtx && ! ir . tpl && ! ir . newlineCfg ) ;
489531 const tplOnly = ! ! ( ir . tpl && ! ir . regexCtx && ! ir . newlineCfg ) ;
490532 const rxTpl = ! ! ( ir . regexCtx && ir . tpl && ! ir . newlineCfg ) ;
533+ const topReuse = topReusePlan ( ir ) ;
491534 const zeroMeta = ', fd: 0, pd: 0, lc: false, lb: false, hd: false, td: 0' ;
492535 const adoptSuffix = `for (let j = oIdx + 1; j < oldToks.length; j++) {
493536 const ot = oldToks[j];
@@ -887,11 +930,121 @@ function checkStreamEq(text: string, meta: AlignMeta[]): boolean {
887930}
888931` ;
889932 const initToks = ( hasNewline || rxOnly || tplOnly || rxTpl ) ? 'scanMeta(src)' : 'toMeta(tokenize(src))' ;
933+ const reuseFns = topReuse ? `
934+ function cstJSON(n: Cst): string {
935+ return JSON.stringify(n, (k, v) => (k === 'tokStart' || k === 'tokEnd' || k === 'ext' ? undefined : v));
936+ }
937+ function checkTreeEq(text: string, root: Node | null): boolean {
938+ const fresh = parse(tokenize(text));
939+ if (root === null || fresh === null) return root === fresh;
940+ return cstJSON(root) === cstJSON(fresh as Cst);
941+ }
942+ function shiftSubtree(n: Cst, byteDelta: number, tokDelta: number): void {
943+ n.offset += byteDelta;
944+ n.end += byteDelta;
945+ n.tokStart += tokDelta;
946+ n.tokEnd += tokDelta;
947+ if ('ext' in n && typeof (n as Node).ext === 'number') (n as Node).ext! += tokDelta;
948+ if ('children' in n) for (const c of n.children) shiftSubtree(c, byteDelta, tokDelta);
949+ }
950+ function tryReuseTop(oldRoot: Node, newText: string, newMeta: AlignMeta[], byteDelta: number, oldN: number, newN: number, prefix: number, suffix: number): { root: Node; reused: number } | null {
951+ const oldKids = oldRoot.children as Node[];
952+ let prefixLen = 0;
953+ while (prefixLen < oldKids.length) {
954+ const k = oldKids[prefixLen]!;
955+ const ext = k.ext ?? k.tokEnd;
956+ if (ext <= prefix) prefixLen++;
957+ else break;
958+ }
959+ let suffixStart = oldKids.length;
960+ for (let i = oldKids.length - 1; i >= prefixLen; i--) {
961+ if (oldKids[i]!.tokStart >= oldN - suffix) suffixStart = i;
962+ else break;
963+ }
964+ const prefixKids = oldKids.slice(0, prefixLen);
965+ const suffixCand = oldKids.slice(suffixStart);
966+ const tokDelta = newN - oldN;
967+ _src = newText;
968+ toks = toksFromMeta(newText, newMeta);
969+ pos = prefixLen > 0 ? prefixKids[prefixLen - 1]!.tokEnd : 0;
970+ const mid: Node[] = [];
971+ const suffixBound = newN - suffix;
972+ const maxCand = suffixCand.length > 0 ? Math.max(...suffixCand.map((k) => k.tokStart + tokDelta)) : -1;
973+ const finish = (adoptFrom: number): { root: Node; reused: number } => {
974+ const adopted = suffixCand.slice(adoptFrom);
975+ for (const s of adopted) shiftSubtree(s, byteDelta, tokDelta);
976+ const children: Cst[] = [...prefixKids, ...mid, ...adopted];
977+ const offset = children.length > 0 ? children[0]!.offset : 0;
978+ const end = children.length > 0 ? children[children.length - 1]!.end : offset;
979+ const tokStart = children.length > 0 ? (children[0] as Node).tokStart : 0;
980+ const tokEnd = children.length > 0 ? (children[children.length - 1] as Node).tokEnd : 0;
981+ return { root: { rule: oldRoot.rule, children, offset, end, tokStart, tokEnd }, reused: prefixKids.length + adopted.length };
982+ };
983+ const tryHit = (): { root: Node; reused: number } | null => {
984+ if (pos < suffixBound) return null;
985+ if (suffixCand.length === 0) {
986+ if (pos === newN) return finish(0);
987+ return null;
988+ }
989+ const hit = suffixCand.findIndex((k) => k.tokStart + tokDelta === pos);
990+ if (hit >= 0) return finish(hit);
991+ return null;
992+ };
993+ {
994+ const early = tryHit();
995+ if (early) return early;
996+ if (suffixCand.length > 0 && maxCand >= 0 && pos > maxCand) return null;
997+ }
998+ for (;;) {
999+ if (pos >= toks.length) {
1000+ if (suffixCand.length === 0 && pos === newN) return finish(0);
1001+ return tryHit() ?? null;
1002+ }
1003+ maxLook = 0;
1004+ const sp = pos;
1005+ const n = parseTopOne();
1006+ if (n === null) { pos = sp; return null; }
1007+ n.ext = Math.max(n.tokEnd, maxLook);
1008+ mid.push(n);
1009+ const hit = tryHit();
1010+ if (hit) return hit;
1011+ if (suffixCand.length > 0 && maxCand >= 0 && pos > maxCand) return null;
1012+ }
1013+ }
1014+ ` : `
1015+ function cstJSON(n: Cst): string {
1016+ return JSON.stringify(n, (k, v) => (k === 'tokStart' || k === 'tokEnd' || k === 'ext' ? undefined : v));
1017+ }
1018+ function checkTreeEq(text: string, root: Node | null): boolean {
1019+ const fresh = parse(tokenize(text));
1020+ if (root === null || fresh === null) return root === fresh;
1021+ return cstJSON(root) === cstJSON(fresh as Cst);
1022+ }
1023+ ` ;
1024+ const editParse = topReuse
1025+ ? ` const byteDelta = text.length - oldText.length;
1026+ let reused = 0;
1027+ let next: Node | null = null;
1028+ if (root !== null) {
1029+ const got = tryReuseTop(root, text, prevToks, byteDelta, core.oldN, core.newN, core.prefix, core.suffix);
1030+ if (got) { next = got.root; reused = got.reused; }
1031+ }
1032+ if (next === null) { _src = text; next = parse(toksFromMeta(text, prevToks)) as Node | null; reused = 0; }
1033+ root = next;
1034+ align = validate
1035+ ? { ...core, reused, streamEq: checkStreamEq(text, prevToks), treeEq: checkTreeEq(text, root) }
1036+ : { ...core, reused };`
1037+ : ` const reused = 0;
1038+ _src = text;
1039+ root = parse(toksFromMeta(text, prevToks)) as Node | null;
1040+ align = validate
1041+ ? { ...core, reused, streamEq: checkStreamEq(text, prevToks), treeEq: checkTreeEq(text, root) }
1042+ : { ...core, reused };` ;
8901043 return `export type Edit = { start: number; end: number; text: string };
8911044type AlignMeta = { kind: string; off: number; end: number; nl: boolean; fd: number; pd: number; lc: boolean; lb: boolean; hd: boolean; td: number };
892- type Align = { oldN: number; newN: number; prefix: number; suffix: number; relexed: number; streamEq?: boolean };
1045+ type Align = { oldN: number; newN: number; prefix: number; suffix: number; relexed: number; reused: number; streamEq?: boolean; treeEq ?: boolean };
8931046${ toMetaFn }
894- function computeAlign(oldText: string, oldToks: AlignMeta[], newText: string, newToks: AlignMeta[]): Omit<Align, 'relexed' | 'streamEq'> {
1047+ function computeAlign(oldText: string, oldToks: AlignMeta[], newText: string, newToks: AlignMeta[]): Omit<Align, 'relexed' | 'reused' | ' streamEq' | 'treeEq '> {
8951048 const oldN = oldToks.length, newN = newToks.length;
8961049 let prefix = 0;
8971050 while (prefix < oldN && prefix < newN) {
@@ -914,12 +1067,12 @@ function computeAlign(oldText: string, oldToks: AlignMeta[], newText: string, ne
9141067function toksFromMeta(text: string, meta: AlignMeta[]): Tok[] {
9151068 return meta.map((m) => ({ kind: m.kind, text: text.slice(m.off, m.end), off: m.off, end: m.end, nl: m.nl }));
9161069}
917- ${ checkStreamEqFn } ${ windowHelpers } export function createDoc(src: string, opts?: { validate?: boolean }): { text(): string; root(): Node | null; align(): Align | null; edit(edits: Edit[]): Node | null } {
1070+ ${ checkStreamEqFn } ${ windowHelpers } ${ reuseFns } export function createDoc(src: string, opts?: { validate?: boolean }): { text(): string; root(): Node | null; align(): Align | null; edit(edits: Edit[]): Node | null } {
9181071 const validate = opts?.validate === true;
9191072 let text = src;
9201073 let prevToks = ${ initToks } ;
9211074 let align: Align | null = null;
922- let root: Node | null = parse(tokenize(src));
1075+ let root: Node | null = parse(tokenize(src)) as Node | null ;
9231076 return {
9241077 text(): string { return text; },
9251078 root(): Node | null { return root; },
@@ -929,8 +1082,7 @@ ${checkStreamEqFn}${windowHelpers}export function createDoc(src: string, opts?:
9291082 let relexed = 0;
9301083${ editBody }
9311084 const core = { ...computeAlign(oldText, oldToks, text, prevToks), relexed };
932- align = validate ? { ...core, streamEq: checkStreamEq(text, prevToks) } : core;
933- root = parse(toksFromMeta(text, prevToks));
1085+ ${ editParse }
9341086 return root;
9351087 },
9361088 };
@@ -956,7 +1108,12 @@ export function tokenize(src: string): Tok[] { return lex(src); }
9561108 } ,
9571109 emitParser ( grammar : CstGrammar , lexerSrc : string | null ) : string {
9581110 const ir = portableIR ( grammar ) ;
959- const ruleFns = ir . rules . map ( ( r ) => ( r . kind === 'pratt' ? prattRule ( r , ir . tpl ) : rdRule ( r ) ) ) . join ( '\n\n' ) ;
1111+ const reuse = topReusePlan ( ir ) ;
1112+ const ruleFns = ir . rules . map ( ( r ) => {
1113+ if ( r . kind === 'pratt' ) return prattRule ( r , ir . tpl ) ;
1114+ if ( reuse && r . name === ir . entry ) return rdEntryWithReuse ( r , reuse ) ;
1115+ return rdRule ( r ) ;
1116+ } ) . join ( '\n\n' ) ;
9601117 const matchTemplate = ir . tpl ? `function matchTemplate(): Cst | null {
9611118 const t = peek();
9621119 if (t === null || t.kind !== '$templateHead') return null;
@@ -981,18 +1138,19 @@ export function tokenize(src: string): Tok[] { return lex(src); }
9811138
9821139type Tok = { kind: string; text: string; off: number; end: number; nl: boolean };
9831140type Leaf = { tokenType: string; offset: number; end: number; tokStart: number; tokEnd: number };
984- type Node = { rule: string; children: Cst[]; offset: number; end: number; tokStart: number; tokEnd: number };
1141+ type Node = { rule: string; children: Cst[]; offset: number; end: number; tokStart: number; tokEnd: number; ext?: number };
9851142type Cst = Node | Leaf;
9861143
9871144${ lexerSrc ?? '' }
9881145
9891146let toks: Tok[] = [];
9901147let pos = 0;
1148+ let maxLook = 0;
9911149let _capped = false;
9921150let _suppressNext: Set<string> | null = null;
9931151let _suppressCur: Set<string> | null = null;
9941152let _src = '';
995- function peek(): Tok | null { return pos < toks.length ? toks[pos] : null; }
1153+ function peek(): Tok | null { maxLook = Math.max(maxLook, pos + 1); return pos < toks.length ? toks[pos] : null; }
9961154function headLeafText(node: Cst): string {
9971155 let n: Cst = node;
9981156 while ('children' in n && n.children.length > 0) n = n.children[0];
@@ -1054,6 +1212,7 @@ export function tokenize(src: string): Tok[] { _src = src; return lex(src); }
10541212export function parse(tokens: Tok[]): Cst | null {
10551213 toks = tokens;
10561214 pos = 0;
1215+ maxLook = 0;
10571216 const root = parse${ ir . entry } ();
10581217 return root !== null && pos === toks.length ? root : null;
10591218}
@@ -1067,7 +1226,7 @@ ${docEditBlock(ir)}
10671226import { readFileSync } from 'node:fs';
10681227const _raw = readFileSync(0, 'utf8');
10691228const _editFast = process.argv.includes('edit-session-fast');
1070- const _cstJSON = (n: Cst) => JSON.stringify(n, (k, v) => (k === 'tokStart' || k === 'tokEnd' ? undefined : v));
1229+ const _cstJSON = (n: Cst) => JSON.stringify(n, (k, v) => (k === 'tokStart' || k === 'tokEnd' || k === 'ext' ? undefined : v));
10711230if (_editFast || process.argv.includes('edit-session')) {
10721231 const { init, batches } = JSON.parse(_raw) as { init: string; batches: [number, number, string][][] };
10731232 const doc = createDoc(init, { validate: !_editFast });
0 commit comments