Skip to content

Commit 909b835

Browse files
committed
Windowed re-lexing: lex O(damage) with depth-recorded restart/resync (M1)
The lexer core is parameterized (lexCore): start anywhere with the previous token's (k, t) as the regex-context seed and empty template/paren stacks. Every token records its two stack depths (tkDp/tkPd columns); the restart anchor is the last token before the damage with both records zero and no live cross-token flag (a control-head ')' or postfix-ambiguous operator), walking back to the file head in the worst case — always sound. The window lexes into the spare buffer set (the old stream stays live), and RESYNC fires at the first token at/past the damage end that aligns with an old token (same k/t, spans shifted by the char delta) at EQUAL stack depths where every still-open bracket was opened BEFORE the damage — the byte-equal prefix guarantees those stack entries agree, while anything opened inside the damage may differ in control-head-ness and must not span the join. The depth-tolerant condition matters: an all-wrapping IIFE (typescript.js) keeps paren depth >= 1 everywhere, and a depth-0-only resync degraded 9MB edits to ~1.2x; with it they reach 2.6x. The splice is copyWithin + a suffix span shift; the damage window is derived from a char-level prefix/suffix compare of the two sources (no edit protocol needed). The true token prefix is recovered by comparing the window's leading tokens against the old stream before the splice, so the memo carry keeps everything the re-lex merely re-derived. Fallback-lexer grammars keep the full-relex path; tokenize() is unchanged for batch (the lexer-equality gate runs the full streams). Numbers: 81KB keystroke 3.5x -> 3.3x parse-side with lex now O(damage); mixed sessions ~1.5-1.65x; 9MB keystroke 2.6x. Remaining per-edit O(n) is the M3/M4 bookkeeping (memo prefix scans, arena re-base loops, suffix span shift) — the green {rel,len} re-base and old-tree cursor adoption kill those next. 30/30 gates; emit≡interp 18,802 byte-identical; reject messages and token streams exact.
1 parent 4b5020a commit 909b835

2 files changed

Lines changed: 169 additions & 42 deletions

File tree

src/emit-lexer.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,32 +199,70 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
199199
emit(`function tokenize(source) {`);
200200
emit(` src = source;`);
201201
emit(` tokN = 0;`);
202+
emit(` lexCore(source, 0, -1, 0, -1, 0, 0);`);
203+
emit(` return tokN;`);
204+
emit(`}`);
205+
emit(`// The lexer core, parameterized for WINDOWED re-lexing: start at startPos with`);
206+
emit(`// the previous token's (k, t) as the regex-context seed (-1 = none / file start)`);
207+
emit(`// and EMPTY template/paren stacks (the caller restarts only at depth-0 safe`);
208+
emit(`// points). In window mode (wndPtr0 >= 0) the OLD stream sits in the alt buffers;`);
209+
emit(`// after each token pushed at/past wndMinOff, resync fires when it aligns with an`);
210+
emit(`// old token (same k/t, offsets shifted by wndDelta, both depth records 0) while`);
211+
emit(`// the window's own stacks are empty — returns that OLD index (the duplicate push`);
212+
emit(`// is retracted), or -1 when lexing ran to EOF.`);
213+
emit(`function lexCore(source, startPos, pvK, pvT, wndPtr0, wndMinOff, wndDelta, wndCs) {`);
202214
emit(` const n = source.length;`);
203-
emit(` let pos = 0;`);
215+
emit(` let pos = startPos;`);
204216
emit(` let pendingNl = false;`);
205217
emit(` let lastBangWasPostfix = false;`);
206218
emit(` let lastCloseWasParenHead = false;`);
207219
emit(` const templateStack = [];`);
208220
emit(` const parenHeadStack = [];`);
221+
emit(` let wndPtr = wndPtr0;`);
222+
emit(` let wndHit = -1;`);
223+
emit(` // stack depths as of the last token fully BEFORE the damage: a resync point may`);
224+
emit(` // sit at any depth as long as every bracket still open there was opened before`);
225+
emit(` // the damage (the prefix agrees byte-for-byte, so those stack entries agree too;`);
226+
emit(` // anything opened inside the damage could differ in control-head-ness).`);
227+
emit(` let dmgDp = -1, dmgPd = -1;`);
228+
emit(` let lastDp = 0, lastPd = 0;`);
209229
emit(` function tkPush(k, t, off, end) {`);
210230
emit(` if (tokN === tkCap) growTok();`);
211231
emit(` tkK[tokN] = k; tkT[tokN] = t; tkOff[tokN] = off; tkEnd[tokN] = end;`);
212232
emit(` tkFl[tokN] = pendingNl ? 1 : 0;`);
233+
emit(` tkDp[tokN] = templateStack.length;`);
234+
emit(` tkPd[tokN] = parenHeadStack.length;`);
213235
emit(` pendingNl = false;`);
236+
emit(` pvK = k; pvT = t;`);
214237
emit(` tokN++;`);
238+
emit(` if (wndPtr >= 0) {`);
239+
emit(` if (dmgPd < 0) {`);
240+
emit(` if (off >= wndCs) { dmgDp = lastDp; dmgPd = lastPd; }`);
241+
emit(` else { lastDp = tkDp[tokN - 1]; lastPd = tkPd[tokN - 1]; }`);
242+
emit(` }`);
243+
emit(` if (off >= wndMinOff && dmgPd >= 0`);
244+
emit(` && templateStack.length <= dmgDp && parenHeadStack.length <= dmgPd) {`);
245+
emit(` while (wndPtr < altN && altOff[wndPtr] + wndDelta < off) wndPtr++;`);
246+
emit(` if (wndPtr < altN && altOff[wndPtr] + wndDelta === off && altK[wndPtr] === k && altT[wndPtr] === t`);
247+
emit(` && altEnd[wndPtr] + wndDelta === end && altDp[wndPtr] === templateStack.length && altPd[wndPtr] === parenHeadStack.length) {`);
248+
emit(` wndHit = wndPtr;`);
249+
emit(` }`);
250+
emit(` }`);
251+
emit(` }`);
215252
emit(` }`);
216253
emit(` // prevIsValue, baked: postfix-ambiguous op → its recorded position; an expression-`);
217254
emit(` // head keyword or a control-head ')' is NOT a value; else division-prev type/text.`);
218255
emit(` function prevIsValue() {`);
219-
emit(` if (tokN === 0) return false;`);
220-
emit(` const i = tokN - 1;`);
221-
emit(` const t = tkT[i];`);
256+
emit(` const k = tokN > 0 ? tkK[tokN - 1] : pvK;`);
257+
emit(` if (k < 0) return false;`);
258+
emit(` const t = tokN > 0 ? tkT[tokN - 1] : pvT;`);
222259
emit(` if (LX_PFXV[t] !== 0) return lastBangWasPostfix;`);
223-
emit(` if (tkK[i] === ${kIdent} && LX_EXPRKW[t] !== 0) return false;`);
260+
emit(` if (k === ${kIdent} && LX_EXPRKW[t] !== 0) return false;`);
224261
emit(` if (t === ${tRParen} && lastCloseWasParenHead) return false;`);
225-
emit(` return LX_DIVK[tkK[i]] !== 0 || LX_DIVT[t] !== 0;`);
262+
emit(` return LX_DIVK[k] !== 0 || LX_DIVT[t] !== 0;`);
226263
emit(` }`);
227264
emit(` while (pos < n) {`);
265+
emit(` if (wndHit >= 0) { tokN--; return wndHit; }`);
228266
emit(` const cc = source.charCodeAt(pos);`);
229267
emit(` // whitespace: ASCII \\s run by char loop; a non-ASCII candidate falls back to the regex`);
230268
emit(` if (cc === 32 || (cc >= 9 && cc <= 13)) {`);
@@ -461,7 +499,21 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
461499
}
462500
emit(` throw new Error("Unexpected character at offset " + pos + ": '" + source[pos] + "'");`);
463501
emit(` }`);
464-
emit(` return tokN;`);
502+
emit(` if (wndHit >= 0) { tokN--; return wndHit; }`);
503+
emit(` return -1;`);
504+
emit(`}`);
505+
emit(`// Windowed-relex restart anchor: the last token B ending at/before the damage`);
506+
emit(`// whose recorded stack depths are zero and whose shape leaves no cross-token`);
507+
emit(`// lexer flag live (a control-head ')' or a postfix-ambiguous operator would`);
508+
emit(`// make the next token's regex-context depend on unrecoverable state). -1 = file`);
509+
emit(`// head (always sound, degrades to a full re-lex).`);
510+
emit(`function findRestart(cs) {`);
511+
emit(` let lo = 0, hi = tokN;`);
512+
emit(` while (lo < hi) { const mid = (lo + hi) >> 1; if (tkEnd[mid] <= cs) lo = mid + 1; else hi = mid; }`);
513+
emit(` for (let b = lo - 1; b >= 0; b--) {`);
514+
emit(` if (tkDp[b] === 0 && tkPd[b] === 0 && LX_PFXV[tkT[b]] === 0 && !(tkK[b] === 1 && tkT[b] === ${tRParen})) return b;`);
515+
emit(` }`);
516+
emit(` return -1;`);
465517
emit(`}`);
466518
return out.join('\n');
467519
}

src/emit-parser.ts

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,12 @@ let tkT = new ${T_ARR}(4096);
13291329
let tkOff = new Int32Array(4096);
13301330
let tkEnd = new Int32Array(4096);
13311331
let tkFl = new Uint8Array(4096);
1332+
// lexer-state depth records per token (windowed relex restart/resync safety):
1333+
// tkDp = template-interp stack depth, tkPd = paren-head stack depth, both AS RECORDED
1334+
// at the token's push (the convention per token kind is fixed by the lexer's code
1335+
// path; determinism is what the predicates rely on, depth-0 is the safe state).
1336+
let tkDp = new Uint8Array(4096);
1337+
let tkPd = new Uint16Array(4096);
13321338
let tkCap = 4096;
13331339
let tokN = 0;
13341340
let src = '';
@@ -1340,6 +1346,8 @@ function growTok() {
13401346
const o = new Int32Array(tkCap); o.set(tkOff); tkOff = o;
13411347
const e2 = new Int32Array(tkCap); e2.set(tkEnd); tkEnd = e2;
13421348
const f = new Uint8Array(tkCap); f.set(tkFl); tkFl = f;
1349+
const d = new Uint8Array(tkCap); d.set(tkDp); tkDp = d;
1350+
const q = new Uint16Array(tkCap); q.set(tkPd); tkPd = q;
13431351
}
13441352
13451353
// ── CST arena: nodes are rows in parallel columns; leaves are TOKEN REFERENCES ──
@@ -1489,6 +1497,8 @@ function matchPuLitGT(pu) {
14891497
tkT.copyWithin(pos + 1, pos, tokN);
14901498
tkOff.copyWithin(pos + 1, pos, tokN);
14911499
tkEnd.copyWithin(pos + 1, pos, tokN);
1500+
tkDp.copyWithin(pos + 1, pos, tokN);
1501+
tkPd.copyWithin(pos + 1, pos, tokN);
14921502
tkFl.copyWithin(pos + 1, pos, tokN);
14931503
${e.soa ? '' : "tkText.splice(pos, 1, '>', restText);"}
14941504
tkT[pos] = pu; tkEnd[pos] = off + 1; tkFl[pos] = 0;
@@ -2099,6 +2109,7 @@ ${e.soa ? ` tokenize(source);` : String.raw` src = source;
20992109
const _t = _toks[_i];
21002110
tkK[_i] = _t.k; tkT[_i] = _t.t; tkOff[_i] = _t.offset; tkEnd[_i] = _t.offset + _t.text.length;
21012111
tkFl[_i] = (_t.newlineBefore ? 1 : 0) | (_t.commentBefore ? 2 : 0) | (_t.multilineFlowBefore ? 4 : 0);
2112+
tkDp[_i] = 0; tkPd[_i] = 0;
21022113
tkText[_i] = _t.text;
21032114
}
21042115
tokN = _n;`}
@@ -2141,7 +2152,20 @@ function runParse(entryRule) {
21412152
let lastSrc = null;
21422153
// The spare token-column buffer set (parseEdited ping-pongs between the live set and
21432154
// this one, so steady-state edits never allocate columns).
2144-
let altK = null, altT = null, altOff = null, altEnd = null, altFl = null;
2155+
let altK = null, altT = null, altOff = null, altEnd = null, altFl = null, altDp = null, altPd = null;
2156+
let altCap = 0;
2157+
let altN = 0; // old-stream token count while a window lex runs (lexCore's resync bound)
2158+
function swapBuffers() {
2159+
let x;
2160+
x = tkK; tkK = altK; altK = x;
2161+
x = tkT; tkT = altT; altT = x;
2162+
x = tkOff; tkOff = altOff; altOff = x;
2163+
x = tkEnd; tkEnd = altEnd; altEnd = x;
2164+
x = tkFl; tkFl = altFl; altFl = x;
2165+
x = tkDp; tkDp = altDp; altDp = x;
2166+
x = tkPd; tkPd = altPd; altPd = x;
2167+
x = tkCap; tkCap = altCap; altCap = x;
2168+
}
21452169
${e.soa ? '' : 'let altText = [];'}
21462170
21472171
export function parse(source, entryRule) {
@@ -2176,46 +2200,106 @@ export function parseEdited(source, entryRule) {
21762200
if (lastSrc === null) return parse(source, entryRule);
21772201
const oSrc = lastSrc;
21782202
lastSrc = null;
2179-
// Stash the old columns BY REFERENCE and lex into the spare buffer set (ping-pong
2180-
// double buffer — steady-state edits allocate nothing and keep the pages warm).
2203+
${e.soa ? String.raw` // ── M1: WINDOWED re-lex ──
2204+
// Char-level envelope (cheapest possible without an edit protocol).
2205+
const oldLen = oSrc.length, newLen = source.length;
2206+
const minL = oldLen < newLen ? oldLen : newLen;
2207+
let cs = 0;
2208+
while (cs < minL && oSrc.charCodeAt(cs) === source.charCodeAt(cs)) cs++;
2209+
let ce = 0;
2210+
while (ce < minL - cs && oSrc.charCodeAt(oldLen - 1 - ce) === source.charCodeAt(newLen - 1 - ce)) ce++;
2211+
const ceOld = oldLen - ce, ceNew = newLen - ce;
2212+
const charDelta = newLen - oldLen;
2213+
// Restart anchor: the last token B ending at/before the damage whose recorded
2214+
// depths are zero and whose shape carries no cross-token lexer flag (')' control-
2215+
// head, postfix-ambiguous op). B = -1 restarts at the file head — always sound.
2216+
const B = findRestart(cs);
2217+
const oN = tokN;
2218+
// first old token at/after the damage end — the resync search floor
2219+
let r0 = oN;
2220+
{ let lo = 0, hi = oN;
2221+
while (lo < hi) { const mid = (lo + hi) >> 1; if (tkOff[mid] < ceOld) lo = mid + 1; else hi = mid; }
2222+
r0 = lo; }
2223+
// Lex the window into the spare buffers (the old stream stays live for resync).
2224+
if (altK === null || altCap < tkCap) {
2225+
altK = new tkK.constructor(tkCap); altT = new tkT.constructor(tkCap);
2226+
altOff = new Int32Array(tkCap); altEnd = new Int32Array(tkCap); altFl = new Uint8Array(tkCap);
2227+
altDp = new Uint8Array(tkCap); altPd = new Uint16Array(tkCap);
2228+
altCap = tkCap;
2229+
}
2230+
altN = oN;
2231+
swapBuffers(); // live = scratch, alt = OLD stream
2232+
src = source;
2233+
tokN = 0;
2234+
const startOff = B >= 0 ? altEnd[B] : 0;
2235+
const R0 = lexCore(source, startOff, B >= 0 ? altK[B] : -1, B >= 0 ? altT[B] : 0, r0, ceNew, charDelta, cs);
2236+
const W = tokN;
2237+
const R = R0 >= 0 ? R0 : oN;
2238+
swapBuffers(); // live = OLD stream again; window sits in the alt buffers
2239+
tokN = oN;
2240+
// TRUE token prefix p: the window re-derives [B+1 .. p) byte-identically; only past
2241+
// p is real damage (compared BEFORE the splice clobbers the old slots).
2242+
let p = B + 1;
2243+
{ let i = 0;
2244+
while (i < W && p < R && altK[i] === tkK[p] && altT[i] === tkT[p] && altOff[i] === tkOff[p]
2245+
&& altEnd[i] === tkEnd[p] && altFl[i] === tkFl[p]) { i++; p++; }
2246+
}
2247+
const dOldEnd = R;
2248+
const tokenDelta = (B + 1 + W) - R;
2249+
const charThresh = R < oN ? tkOff[R] : 0x7fffffff;
2250+
// ── splice: old[0..B] + window[0..W) + old[R..oN), then shift the suffix spans ──
2251+
const nN = B + 1 + W + (oN - R);
2252+
while (tkCap < nN + 1) growTok();
2253+
tkK.copyWithin(B + 1 + W, R, oN); tkT.copyWithin(B + 1 + W, R, oN);
2254+
tkOff.copyWithin(B + 1 + W, R, oN); tkEnd.copyWithin(B + 1 + W, R, oN);
2255+
tkFl.copyWithin(B + 1 + W, R, oN); tkDp.copyWithin(B + 1 + W, R, oN); tkPd.copyWithin(B + 1 + W, R, oN);
2256+
if (W > 0) {
2257+
tkK.set(altK.subarray(0, W), B + 1); tkT.set(altT.subarray(0, W), B + 1);
2258+
tkOff.set(altOff.subarray(0, W), B + 1); tkEnd.set(altEnd.subarray(0, W), B + 1);
2259+
tkFl.set(altFl.subarray(0, W), B + 1); tkDp.set(altDp.subarray(0, W), B + 1); tkPd.set(altPd.subarray(0, W), B + 1);
2260+
}
2261+
if (charDelta !== 0) {
2262+
for (let i = B + 1 + W; i < nN; i++) { tkOff[i] += charDelta; tkEnd[i] += charDelta; }
2263+
}
2264+
tokN = nN;
2265+
const nN2 = nN;
2266+
const oN2 = oN;` : String.raw` // (fallback-lexer grammars keep the full-relex + token-diff path)
21812267
const oK = tkK, oT = tkT, oOff = tkOff, oEnd = tkEnd, oFl = tkFl, oN = tokN;
2182-
${e.soa ? '' : ' const oText = tkText;'}
2268+
const oText = tkText;
21832269
if (altK === null || altK.length !== tkCap) {
21842270
altK = new tkK.constructor(tkCap); altT = new tkT.constructor(tkCap);
21852271
altOff = new Int32Array(tkCap); altEnd = new Int32Array(tkCap); altFl = new Uint8Array(tkCap);
2272+
altDp = new Uint8Array(tkCap); altPd = new Uint16Array(tkCap);
21862273
}
21872274
tkK = altK; tkT = altT; tkOff = altOff; tkEnd = altEnd; tkFl = altFl;
2188-
${e.soa ? '' : ' tkText = altText; tkText.length = 0;'}
2275+
{ const _d = tkDp; tkDp = altDp; altDp = _d; const _q = tkPd; tkPd = altPd; altPd = _q; }
2276+
tkText = altText; tkText.length = 0;
21892277
altK = oK; altT = oT; altOff = oOff; altEnd = oEnd; altFl = oFl;
2190-
${e.soa ? '' : ' altText = oText;'}
2278+
altText = oText;
21912279
lexInto(source);
2192-
if (tkCap !== oK.length) {
2193-
// the new lex outgrew the buffer (growTok reallocated): drop the stale spare
2194-
altK = null;
2195-
}
21962280
const nN = tokN;
21972281
const charDelta = source.length - oSrc.length;
21982282
const minN = oN < nN ? oN : nN;
2199-
// Longest identical prefix (positions included — the prefix is unshifted).
22002283
let p = 0;
22012284
while (p < minN && oK[p] === tkK[p] && oT[p] === tkT[p] && oFl[p] === tkFl[p]
2202-
&& oOff[p] === tkOff[p] && oEnd[p] === tkEnd[p]${e.soa ? '' : ' && oText[p] === tkText[p]'}) p++;
2203-
// Longest identical suffix modulo charDelta (disjoint from the prefix).
2285+
&& oOff[p] === tkOff[p] && oEnd[p] === tkEnd[p] && oText[p] === tkText[p]) p++;
22042286
let s = 0;
22052287
while (s < minN - p) {
22062288
const i = oN - 1 - s, j = nN - 1 - s;
22072289
if (oK[i] === tkK[j] && oT[i] === tkT[j] && oFl[i] === tkFl[j]
2208-
&& oOff[i] + charDelta === tkOff[j] && oEnd[i] + charDelta === tkEnd[j]${e.soa ? '' : ' && oText[i] === tkText[j]'}) s++;
2290+
&& oOff[i] + charDelta === tkOff[j] && oEnd[i] + charDelta === tkEnd[j] && oText[i] === tkText[j]) s++;
22092291
else break;
22102292
}
2211-
const dOldEnd = oN - s; // damaged OLD tokens: [p, dOldEnd)
2293+
const dOldEnd = oN - s;
22122294
const tokenDelta = nN - oN;
2213-
// Re-base the old arena in place: rows starting at/after the first suffix token's OLD
2214-
// offset shift by charDelta; reused leaf entries past the damage shift by tokenDelta.
2215-
// (A reusable subtree lies entirely on one side of the damage, so the start-threshold
2216-
// classifies it correctly; damage-spanning rows are garbage either way.)
2217-
if (s > 0 && (charDelta !== 0 || tokenDelta !== 0)) {
2218-
const charThresh = oOff[dOldEnd];
2295+
const charThresh = s > 0 ? oOff[dOldEnd] : 0x7fffffff;
2296+
const nN2 = nN;
2297+
const oN2 = oN;`}
2298+
// Re-base the old arena in place: rows starting at/after the first kept-suffix
2299+
// token's OLD offset shift by charDelta; reused leaf entries past the damage shift
2300+
// by tokenDelta. (A reusable subtree lies entirely on one side of the damage; rows
2301+
// spanning it are unreachable garbage either way.)
2302+
if (dOldEnd < oN2 && (charDelta !== 0 || tokenDelta !== 0)) {
22192303
if (charDelta !== 0) {
22202304
for (let i = 0; i < nodeN; i++) if (rowOff[i] >= charThresh) rowOff[i] += charDelta;
22212305
}
@@ -2227,18 +2311,12 @@ ${e.soa ? '' : ' altText = oText;'}
22272311
}
22282312
}
22292313
}
2230-
// Carry the memo across: prefix entries whose lookahead never reached the damage stay
2231-
// at their index; suffix entries move by tokenDelta (ids reference the re-based rows).
2232-
// tokenDelta === 0 (the common keystroke: editing within a token) mutates IN PLACE —
2233-
// no per-rule array allocation; only the damage window and the prefix entries whose
2234-
// extent crossed into it are cleared.
2314+
// Carry the memo across: prefix entries whose lookahead never reached the damage
2315+
// stay; suffix entries shift by tokenDelta; the damage window drops.
22352316
for (let r = 0; r < MEMO_RULES; r++) {
22362317
const me = memoEnd[r];
22372318
if (me === undefined) continue;
22382319
const mn = memoNode[r], mx = memoExt[r];
2239-
// prefix entries whose lookahead may have crossed into the damage die in place
2240-
// (mx is the advance watermark; reads run up to two tokens past it: the stop
2241-
// token and the SECOND-token dispatch probe)
22422320
for (let i = 0; i < p; i++) {
22432321
if (me[i] !== undefined && mx[i] + 2 > p) { me[i] = undefined; mn[i] = undefined; mx[i] = undefined; }
22442322
}
@@ -2248,15 +2326,12 @@ ${e.soa ? '' : ' altText = oText;'}
22482326
}
22492327
continue;
22502328
}
2251-
// token count changed: rebuild the rule's arrays sparsely (measured FASTER than an
2252-
// in-place direction-aware shift — writing undefined through the holes materializes
2253-
// them; fresh holey arrays skip that entirely).
2254-
const nme = new Array(nN + 1), nmn = new Array(nN + 1), nmx = new Array(nN + 1);
2255-
const pCap = p < nN + 1 ? p : nN + 1;
2329+
const nme = new Array(nN2 + 1), nmn = new Array(nN2 + 1), nmx = new Array(nN2 + 1);
2330+
const pCap = p < nN2 + 1 ? p : nN2 + 1;
22562331
for (let i = 0; i < pCap; i++) {
22572332
if (me[i] !== undefined) { nme[i] = me[i]; nmn[i] = mn[i]; nmx[i] = mx[i]; }
22582333
}
2259-
for (let i = dOldEnd; i <= oN; i++) {
2334+
for (let i = dOldEnd; i <= oN2; i++) {
22602335
if (me[i] !== undefined) {
22612336
const j = i + tokenDelta;
22622337
nme[j] = me[i] + tokenDelta; nmn[j] = mn[i]; nmx[j] = mx[i] + tokenDelta;

0 commit comments

Comments
 (0)