@@ -103,6 +103,11 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
103103 emit ( `// ── Emitted lexer (emit-lexer.ts): specialized tokenize for this grammar ──` ) ;
104104 for ( const m of matchers ) emit ( `const ${ m . re } = new RegExp(${ J ( `(?:${ m . pattern } )` ) } , ${ J ( m . flags ) } );` ) ;
105105 emit ( `const LX_WS = /\\s+/y;` ) ;
106+ emit ( `// window-truncation retry: a matcher failing at the WINDOW edge is not a lex` ) ;
107+ emit ( `// error — the caller re-materializes a larger window (truncation cannot fake a` ) ;
108+ emit ( `// resync: suffix-zone equality makes a cut token's END mismatch the old one)` ) ;
109+ emit ( `const LEX_RETRY = { retry: true };` ) ;
110+ emit ( `let lexWindowMore = false;` ) ;
106111 emit ( `const LX_UNI_IDENT = /[$_\\p{ID_Start}][$\\u200c\\u200d\\p{ID_Continue}]*/uy;` ) ;
107112 emit ( `const LX_UNI_CONT = /[$\\u200c\\u200d\\p{ID_Continue}]+/uy;` ) ;
108113 emit ( `const LX_UNI_FULL = /^[$_\\p{ID_Start}][$\\u200c\\u200d\\p{ID_Continue}]*/u;` ) ;
@@ -177,7 +182,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
177182 emit ( ` if (validateEscapes) {` ) ;
178183 emit ( ` LX_TPL_ESC.lastIndex = pos;` ) ;
179184 emit ( ` const m = LX_TPL_ESC.exec(source);` ) ;
180- emit ( ` if (!m) throw new Error('Invalid escape sequence in template at offset ' + pos);` ) ;
185+ emit ( ` if (!m) { if (lexWindowMore) throw LEX_RETRY; throw new Error('Invalid escape sequence in template at offset ' + pos); } ` ) ;
181186 emit ( ` pos += m[0].length;` ) ;
182187 emit ( ` } else { pos += 2; }` ) ;
183188 } else {
@@ -188,6 +193,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
188193 emit ( ` if (${ startsWithExpr ( 'source' , 'pos' , tplOpen ) } ) return { endsWithInterp: false, end: pos + ${ tplOpen . length } };` ) ;
189194 emit ( ` pos++;` ) ;
190195 emit ( ` }` ) ;
196+ emit ( ` if (lexWindowMore) throw LEX_RETRY;` ) ;
191197 emit ( ` throw new Error('Unterminated template literal at offset ' + pos);` ) ;
192198 emit ( `}` ) ;
193199 }
@@ -197,34 +203,81 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
197203 // when a CST leaf is built. Flag bits: 1 = newlineBefore (the only stamp this emitted
198204 // lexer ever sets; comment/multilineFlow stamps belong to fallback-only grammars).
199205 emit ( `function tokenize(source) {` ) ;
200- emit ( ` src = source;` ) ;
206+ emit ( ` docPieces = [source]; docPieceOff = [0]; docLen = source.length;` ) ;
207+ emit ( ` docFlat = source; docCur = 0;` ) ;
201208 emit ( ` tokN = 0;` ) ;
209+ emit ( ` parenCachePos = -1;` ) ;
210+ emit ( ` srcLenP1 = source.length + 1;` ) ;
211+ emit ( ` negFrom = 0x7fffffff;` ) ;
212+ emit ( ` lexCore(source, 0, -1, 0, -1, 0, 0);` ) ;
213+ emit ( ` return tokN;` ) ;
214+ emit ( `}` ) ;
215+ emit ( `// The lexer core, parameterized for WINDOWED re-lexing: start at startPos with` ) ;
216+ emit ( `// the previous token's (k, t) as the regex-context seed (-1 = none / file start)` ) ;
217+ emit ( `// and EMPTY template/paren stacks (the caller restarts only at depth-0 safe` ) ;
218+ emit ( `// points). In window mode (wndPtr0 >= 0) the OLD stream sits in the alt buffers;` ) ;
219+ emit ( `// after each token pushed at/past wndMinOff, resync fires when it aligns with an` ) ;
220+ emit ( `// old token (same k/t, offsets shifted by wndDelta, both depth records 0) while` ) ;
221+ emit ( `// the window's own stacks are empty — returns that OLD index (the duplicate push` ) ;
222+ emit ( `// is retracted), or -1 when lexing ran to EOF.` ) ;
223+ emit ( `function lexCore(source, startPos, pvK, pvT, wndPtr0, wndMinOff, wndDelta, wndCs, initParens, srcBase, hasMore) {` ) ;
224+ emit ( ` if (srcBase === undefined) srcBase = 0;` ) ;
225+ emit ( ` lexWindowMore = hasMore === true;` ) ;
202226 emit ( ` const n = source.length;` ) ;
203- emit ( ` let pos = 0 ;` ) ;
227+ emit ( ` let pos = startPos ;` ) ;
204228 emit ( ` let pendingNl = false;` ) ;
229+ emit ( ` let extraFl = 0;` ) ;
205230 emit ( ` let lastBangWasPostfix = false;` ) ;
206231 emit ( ` let lastCloseWasParenHead = false;` ) ;
207232 emit ( ` const templateStack = [];` ) ;
208- emit ( ` const parenHeadStack = [];` ) ;
233+ emit ( ` const parenHeadStack = initParens !== undefined && initParens !== null ? initParens : [];` ) ;
234+ emit ( ` let wndPtr = wndPtr0;` ) ;
235+ emit ( ` let wndHit = -1;` ) ;
236+ emit ( ` // stack depths as of the last token fully BEFORE the damage: a resync point may` ) ;
237+ emit ( ` // sit at any depth as long as every bracket still open there was opened before` ) ;
238+ emit ( ` // the damage (the prefix agrees byte-for-byte, so those stack entries agree too;` ) ;
239+ emit ( ` // anything opened inside the damage could differ in control-head-ness).` ) ;
240+ emit ( ` let dmgDp = -1, dmgPd = -1;` ) ;
241+ emit ( ` let lastDp = templateStack.length, lastPd = parenHeadStack.length;` ) ;
209242 emit ( ` function tkPush(k, t, off, end) {` ) ;
243+ emit ( ` off += srcBase; end += srcBase;` ) ;
210244 emit ( ` if (tokN === tkCap) growTok();` ) ;
211245 emit ( ` tkK[tokN] = k; tkT[tokN] = t; tkOff[tokN] = off; tkEnd[tokN] = end;` ) ;
212- emit ( ` tkFl[tokN] = pendingNl ? 1 : 0;` ) ;
246+ emit ( ` tkFl[tokN] = (pendingNl ? 1 : 0) | extraFl;` ) ;
247+ emit ( ` extraFl = 0;` ) ;
248+ emit ( ` tkDp[tokN] = templateStack.length;` ) ;
249+ emit ( ` tkPd[tokN] = parenHeadStack.length;` ) ;
213250 emit ( ` pendingNl = false;` ) ;
251+ emit ( ` pvK = k; pvT = t;` ) ;
214252 emit ( ` tokN++;` ) ;
253+ emit ( ` if (wndPtr >= 0) {` ) ;
254+ emit ( ` if (dmgPd < 0) {` ) ;
255+ emit ( ` if (off >= wndCs) { dmgDp = lastDp; dmgPd = lastPd; }` ) ;
256+ emit ( ` else { lastDp = tkDp[tokN - 1]; lastPd = tkPd[tokN - 1]; }` ) ;
257+ emit ( ` }` ) ;
258+ emit ( ` if (off >= wndMinOff && dmgPd >= 0` ) ;
259+ emit ( ` && templateStack.length <= dmgDp && parenHeadStack.length <= dmgPd) {` ) ;
260+ emit ( ` while (wndPtr < altN && (altOff[wndPtr] < 0 ? altOff[wndPtr] + srcLenP1 : altOff[wndPtr]) + wndDelta < off) wndPtr++;` ) ;
261+ emit ( ` if (wndPtr < altN && (altOff[wndPtr] < 0 ? altOff[wndPtr] + srcLenP1 : altOff[wndPtr]) + wndDelta === off && altK[wndPtr] === k && altT[wndPtr] === t` ) ;
262+ emit ( ` && (altEnd[wndPtr] < 0 ? altEnd[wndPtr] + srcLenP1 : altEnd[wndPtr]) + wndDelta === end && altDp[wndPtr] === templateStack.length && altPd[wndPtr] === parenHeadStack.length) {` ) ;
263+ emit ( ` wndHit = wndPtr;` ) ;
264+ emit ( ` }` ) ;
265+ emit ( ` }` ) ;
266+ emit ( ` }` ) ;
215267 emit ( ` }` ) ;
216268 emit ( ` // prevIsValue, baked: postfix-ambiguous op → its recorded position; an expression-` ) ;
217269 emit ( ` // head keyword or a control-head ')' is NOT a value; else division-prev type/text.` ) ;
218270 emit ( ` function prevIsValue() {` ) ;
219- emit ( ` if (tokN === 0) return false ;` ) ;
220- emit ( ` const i = tokN - 1 ;` ) ;
221- emit ( ` const t = tkT[i] ;` ) ;
271+ emit ( ` const k = tokN > 0 ? tkK[tokN - 1] : pvK ;` ) ;
272+ emit ( ` if (k < 0) return false ;` ) ;
273+ emit ( ` const t = tokN > 0 ? tkT[tokN - 1] : pvT ;` ) ;
222274 emit ( ` if (LX_PFXV[t] !== 0) return lastBangWasPostfix;` ) ;
223- emit ( ` if (tkK[i] === ${ kIdent } && LX_EXPRKW[t] !== 0) return false;` ) ;
275+ emit ( ` if (k === ${ kIdent } && LX_EXPRKW[t] !== 0) return false;` ) ;
224276 emit ( ` if (t === ${ tRParen } && lastCloseWasParenHead) return false;` ) ;
225- emit ( ` return LX_DIVK[tkK[i] ] !== 0 || LX_DIVT[t] !== 0;` ) ;
277+ emit ( ` return LX_DIVK[k ] !== 0 || LX_DIVT[t] !== 0;` ) ;
226278 emit ( ` }` ) ;
227279 emit ( ` while (pos < n) {` ) ;
280+ emit ( ` if (wndHit >= 0) { tokN--; return wndHit; }` ) ;
228281 emit ( ` const cc = source.charCodeAt(pos);` ) ;
229282 emit ( ` // whitespace: ASCII \\s run by char loop; a non-ASCII candidate falls back to the regex` ) ;
230283 emit ( ` if (cc === 32 || (cc >= 9 && cc <= 13)) {` ) ;
@@ -317,7 +370,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
317370 emit ( `${ ind } if (m !== null) {` ) ;
318371 if ( m . identLike ) {
319372 const plen = ( identPrefixByName . get ( m . name ) ?? '' ) . length ;
320- emit ( `${ ind } if (!lexIdentValid(m[0], ${ plen } )) throw new Error("Invalid identifier escape at offset " + pos + ": '" + m[0] + "'");` ) ;
373+ emit ( `${ ind } if (!lexIdentValid(m[0], ${ plen } )) { if (lexWindowMore) throw LEX_RETRY; throw new Error("Invalid identifier escape at offset " + pos + ": '" + m[0] + "'"); } ` ) ;
321374 }
322375 if ( m . skip ) {
323376 emit ( `${ ind } if (m[0].includes('\\n')) pendingNl = true;` ) ;
@@ -334,7 +387,9 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
334387 // Chars 1..len-1 already known to match when this leaf is reached via the chain below.
335388 if ( lit === '(' ) {
336389 emit ( `${ ind } { const isMemberName = tokN >= 2 && LX_MEMBER[tkT[tokN - 2]] !== 0;` ) ;
337- emit ( `${ ind } parenHeadStack.push(!isMemberName && tokN >= 1 && tkK[tokN - 1] === ${ kIdent } && LX_PARENKW[tkT[tokN - 1]] !== 0); }` ) ;
390+ emit ( `${ ind } const _ph = !isMemberName && tokN >= 1 && tkK[tokN - 1] === ${ kIdent } && LX_PARENKW[tkT[tokN - 1]] !== 0;` ) ;
391+ emit ( `${ ind } parenHeadStack.push(_ph);` ) ;
392+ emit ( `${ ind } extraFl = _ph ? 8 : 0; }` ) ;
338393 } else if ( lit === ')' ) {
339394 emit ( `${ ind } lastCloseWasParenHead = parenHeadStack.pop() ?? false;` ) ;
340395 }
@@ -425,13 +480,13 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
425480 emit ( ` const _li = tokN - 1;` ) ;
426481 const likeKs = [ ...identLike ] . map ( kOf ) ;
427482 const likeCond = likeKs . map ( k => `tkK[_li] === ${ k } ` ) . join ( ' || ' ) ;
428- emit ( ` if ((${ likeCond } ) && tkEnd[_li] === pos) {` ) ;
483+ emit ( ` if ((${ likeCond } ) && tkEnd[_li] === pos + srcBase ) {` ) ;
429484 emit ( ` LX_UNI_CONT.lastIndex = pos;` ) ;
430485 emit ( ` const cont = LX_UNI_CONT.exec(source);` ) ;
431486 emit ( ` if (cont !== null) {` ) ;
432487 emit ( ` pos += cont[0].length;` ) ;
433- emit ( ` tkEnd[_li] = pos;` ) ;
434- emit ( ` tkT[_li] = lexKwT(source, tkOff[_li], pos);` ) ;
488+ emit ( ` tkEnd[_li] = pos + srcBase ;` ) ;
489+ emit ( ` tkT[_li] = lexKwT(source, tkOff[_li] - srcBase , pos);` ) ;
435490 emit ( ` continue;` ) ;
436491 emit ( ` }` ) ;
437492 emit ( ` }` ) ;
@@ -459,9 +514,70 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
459514 emit ( ` }` ) ;
460515 emit ( ` }` ) ;
461516 }
517+ emit ( ` if (lexWindowMore) throw LEX_RETRY;` ) ;
462518 emit ( ` throw new Error("Unexpected character at offset " + pos + ": '" + source[pos] + "'");` ) ;
463519 emit ( ` }` ) ;
464- emit ( ` return tokN;` ) ;
520+ emit ( ` if (wndHit >= 0) { tokN--; return wndHit; }` ) ;
521+ emit ( ` return hasMore ? -2 : -1;` ) ;
522+ emit ( `}` ) ;
523+ emit ( `// Windowed-relex restart anchor: the last token B ending at/before the damage` ) ;
524+ emit ( `// whose recorded stack depths are zero and whose shape leaves no cross-token` ) ;
525+ emit ( `// lexer flag live (a control-head ')' or a postfix-ambiguous operator would` ) ;
526+ emit ( `// make the next token's regex-context depend on unrecoverable state). -1 = file` ) ;
527+ emit ( `// head (always sound, degrades to a full re-lex).` ) ;
528+ emit ( `function findRestart(cs) {` ) ;
529+ emit ( ` let lo = 0, hi = tokN;` ) ;
530+ // STRICTLY before the damage: a token ENDING exactly at cs can be EXTENDED by
531+ // the edit under maximal munch ('b' + inserted 'x' = 'bx'; '=' + '=' = '==';
532+ // deleting the gap glues neighbours) and the anchor itself is never re-lexed —
533+ // with < the abutting token falls inside the window and the merge is re-derived.
534+ emit ( ` while (lo < hi) { const mid = (lo + hi) >> 1; if (tend(mid) < cs) lo = mid + 1; else hi = mid; }` ) ;
535+ emit ( ` for (let b = lo - 1; b >= 0; b--) {` ) ;
536+ emit ( ` // template depth must be zero (interp brace counters are not reconstructable),` ) ;
537+ emit ( ` // and the anchor token must leave no cross-token lexer flag live: not a` ) ;
538+ emit ( ` // control-head ')', not a postfix-ambiguous op, and not a control KEYWORD` ) ;
539+ emit ( ` // (a '(' lexed first in the window would mis-derive its head-ness from a` ) ;
540+ emit ( ` // missing predecessor). Paren depth may be anything — the live stack is` ) ;
541+ emit ( ` // reconstructed from the recorded depths and the '(' head bits.` ) ;
542+ emit ( ` if (tkDp[b] === 0 && LX_PFXV[tkT[b]] === 0 && LX_PARENKW[tkT[b]] === 0 && !(tkK[b] === 1 && tkT[b] === ${ tRParen } )) return b;` ) ;
543+ emit ( ` }` ) ;
544+ emit ( ` return -1;` ) ;
545+ emit ( `}` ) ;
546+ emit ( `// Rebuild the live paren-head stack enclosing token b: scanning backward, the` ) ;
547+ emit ( `// first '(' recording exactly depth d is the live opener of level d (closed` ) ;
548+ emit ( `// openers at that depth are re-opened later, and the re-opener comes first` ) ;
549+ emit ( `// backward). The '(' records its depth INCLUDING itself, and carries its` ) ;
550+ emit ( `// control-head-ness as tkFl bit 8.` ) ;
551+ emit ( `function reconstructParens(b) {` ) ;
552+ emit ( ` let need = b >= 0 ? tkPd[b] : 0;` ) ;
553+ emit ( ` const out = new Array(need);` ) ;
554+ emit ( ` for (let i = b; i >= 0 && need > 0; i--) {` ) ;
555+ emit ( ` if (tkK[i] === 1 && tkT[i] === ${ tOf ( '(' ) } && tkPd[i] === need) { out[need - 1] = (tkFl[i] & 8) !== 0; need--; }` ) ;
556+ emit ( ` }` ) ;
557+ emit ( ` return out;` ) ;
558+ emit ( `}` ) ;
559+ emit ( `// Session cache for the live paren stack: the previous edit's anchor stack rolled` ) ;
560+ emit ( `// FORWARD over the tokens between the two anchors (push on '(', pop on ')') — the` ) ;
561+ emit ( `// backward scan is O(distance to the outermost live opener), which a deep` ) ;
562+ emit ( `// stationary session would pay per keystroke. Tokens at/before the cached anchor` ) ;
563+ emit ( `// are splice-stable (every splice begins past its own anchor), so the baseline` ) ;
564+ emit ( `// stays exact; a backward jump (b < cached) falls back to the full scan.` ) ;
565+ emit ( `let parenCachePos = -1;` ) ;
566+ emit ( `let parenCacheStack = [];` ) ;
567+ emit ( `function reconstructParensCached(b) {` ) ;
568+ emit ( ` let stack;` ) ;
569+ emit ( ` if (b < 0) stack = [];` ) ;
570+ emit ( ` else if (parenCachePos >= 0 && parenCachePos <= b) {` ) ;
571+ emit ( ` stack = parenCacheStack;` ) ;
572+ emit ( ` for (let i = parenCachePos + 1; i <= b; i++) {` ) ;
573+ emit ( ` if (tkK[i] === 1) {` ) ;
574+ emit ( ` if (tkT[i] === ${ tOf ( '(' ) } ) stack.push((tkFl[i] & 8) !== 0);` ) ;
575+ emit ( ` else if (tkT[i] === ${ tRParen } ) { if (stack.length > 0) stack.pop(); }` ) ;
576+ emit ( ` }` ) ;
577+ emit ( ` }` ) ;
578+ emit ( ` } else stack = reconstructParens(b);` ) ;
579+ emit ( ` parenCachePos = b; parenCacheStack = stack;` ) ;
580+ emit ( ` return stack.slice();` ) ;
465581 emit ( `}` ) ;
466582 return out . join ( '\n' ) ;
467583}
0 commit comments