Skip to content

Commit 70a8019

Browse files
Incremental re-parse: O(damage) edits, a 9MB keystroke in ~0.05ms, proven ≡ fresh (#38)
1 parent 4945c5d commit 70a8019

15 files changed

Lines changed: 2493 additions & 172 deletions

src/emit-lexer.ts

Lines changed: 132 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)