@@ -28,6 +28,23 @@ export interface LexerSymtab {
2828
2929const J = ( v : unknown ) => JSON . stringify ( v ) ;
3030
31+ // The resync retract one-liner is emitted at two points in the relex loop (mid-loop and the
32+ // post-loop EOF check); a single producer keeps the two from drifting (#45 B3).
33+ const resyncRetractLine = ( indent : string ) : string =>
34+ `${ indent } if (wndHit >= 0) { tokN--; while (docLex.length > lexDiagBase && docLex[docLex.length - 1].offset >= tkOff[tokN]) docLex.length--; return wndHit; }` ;
35+
36+ // The non-ASCII members of JS \s (the /u-free set), baked as a charCode test so a
37+ // non-whitespace cc>127 (e.g. a Unicode identifier char) skips the LX_WS regex entirely. The
38+ // regex `/\s+/y` matches at pos iff the lead char is \s, and ASCII \s is handled by the char
39+ // loop, so `cc>127 && lxNonAsciiWs(cc)` is EXACTLY "the regex would match here" → byte-
40+ // identical, minus the wasted exec on the common non-whitespace case (#45 B4).
41+ const NON_ASCII_WS_FN =
42+ `function lxNonAsciiWs(cc) { return cc === 0xa0 || cc === 0x1680 || (cc >= 0x2000 && cc <= 0x200a) || cc === 0x2028 || cc === 0x2029 || cc === 0x202f || cc === 0x205f || cc === 0x3000 || cc === 0xfeff; }` ;
43+ // The non-ASCII whitespace fallback, emitted at the two sites that need it (after an ASCII run,
44+ // and as the lead char). `cont` appends the `continue` the lead-char site needs.
45+ const nonAsciiWsConsume = ( v : string , cont : boolean , indent : string ) : string =>
46+ `${ indent } if (${ v } > 127 && lxNonAsciiWs(${ v } )) { LX_WS.lastIndex = pos; const m = LX_WS.exec(source); if (m !== null) { if (m[0].includes('\\n')) pendingNl = true; pos += m[0].length;${ cont ? ' continue;' : '' } } }` ;
47+
3148export function emitLexer ( grammar : CstGrammar , st : LexerSymtab ) : string | null {
3249 // Out of scope: the markup / indentation / newline state machines.
3350 if ( grammar . markup || grammar . indent || grammar . newline ) return null ;
@@ -103,6 +120,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
103120 emit ( `// ── Emitted lexer (emit-lexer.ts): specialized tokenize for this grammar ──` ) ;
104121 for ( const m of matchers ) emit ( `const ${ m . re } = new RegExp(${ J ( `(?:${ m . pattern } )` ) } , ${ J ( m . flags ) } );` ) ;
105122 emit ( `const LX_WS = /\\s+/y;` ) ;
123+ emit ( NON_ASCII_WS_FN ) ;
106124 emit ( `// window-truncation retry: a matcher failing at the WINDOW edge is not a lex` ) ;
107125 emit ( `// error — the caller re-materializes a larger window (truncation cannot fake a` ) ;
108126 emit ( `// resync: suffix-zone equality makes a cut token's END mismatch the old one)` ) ;
@@ -248,6 +266,13 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
248266 emit ( ` lexCore(source, 0, -1, 0, -1, 0, 0);` ) ;
249267 emit ( ` return tokN;` ) ;
250268 emit ( `}` ) ;
269+ // Verification of the WINDOWED path (issue #45 B2): emit-lexer-verify only exercises a FULL
270+ // lex (emit ≡ createLexer), and gen-lexer has no windowed counterpart to diff against — but the
271+ // windowed re-lex IS independently checked at the tree level. incremental-verify / exhaustive-
272+ // edits compare an edited parse (whose tokens come from this windowed re-lex) to a FRESH FULL
273+ // parse of the same text, byte-identical: a wrong windowed token would change the tree (or its
274+ // newlineBefore/commentBefore-driven shape) and fail there. So the oracle is the fresh full
275+ // parse, applied transitively through the parser.
251276 emit ( `// The lexer core, parameterized for WINDOWED re-lexing: start at startPos with` ) ;
252277 emit ( `// the previous token's (k, t) as the regex-context seed (-1 = none / file start)` ) ;
253278 emit ( `// and EMPTY template/paren stacks (the caller restarts only at depth-0 safe` ) ;
@@ -359,7 +384,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
359384 emit ( ` // resync retracts the duplicated token push — and any lexer diagnostics
360385 // emitted FOR it (the old stream's persisted entry survives via the shift;
361386 // keeping the window's copy too double-reports the same character)` ) ;
362- emit ( ` if (wndHit >= 0) { tokN--; while (docLex.length > lexDiagBase && docLex[docLex.length - 1].offset >= tkOff[tokN]) docLex.length--; return wndHit; }` ) ;
387+ emit ( resyncRetractLine ( ' ' ) ) ;
363388 emit ( ` const cc = source.charCodeAt(pos);` ) ;
364389 emit ( ` // whitespace: ASCII \\s run by char loop; a non-ASCII candidate falls back to the regex` ) ;
365390 emit ( ` if (cc === 32 || (cc >= 9 && cc <= 13)) {` ) ;
@@ -369,18 +394,10 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
369394 emit ( ` pos++;` ) ;
370395 emit ( ` wc = source.charCodeAt(pos);` ) ;
371396 emit ( ` } while (wc === 32 || (wc >= 9 && wc <= 13));` ) ;
372- emit ( ` if (wc > 127) {` ) ;
373- emit ( ` LX_WS.lastIndex = pos;` ) ;
374- emit ( ` const m = LX_WS.exec(source);` ) ;
375- emit ( ` if (m !== null) { if (m[0].includes('\\n')) pendingNl = true; pos += m[0].length; }` ) ;
376- emit ( ` }` ) ;
397+ emit ( `${ nonAsciiWsConsume ( 'wc' , false , ' ' ) } ` ) ;
377398 emit ( ` continue;` ) ;
378399 emit ( ` }` ) ;
379- emit ( ` if (cc > 127) {` ) ;
380- emit ( ` LX_WS.lastIndex = pos;` ) ;
381- emit ( ` const m = LX_WS.exec(source);` ) ;
382- emit ( ` if (m !== null) { if (m[0].includes('\\n')) pendingNl = true; pos += m[0].length; continue; }` ) ;
383- emit ( ` }` ) ;
400+ emit ( `${ nonAsciiWsConsume ( 'cc' , true , ' ' ) } ` ) ;
384401 if ( templateToken ) {
385402 const tplCloseT = kwFirstCcs . has ( tplInterpClose . charCodeAt ( 0 ) ) ? 'lexKwT(source, startPos, r.end)' : '0' ;
386403 const tplOpenT = kwFirstCcs . has ( tplOpen . charCodeAt ( 0 ) ) ? 'lexKwT(source, startPos, r.end)' : '0' ;
@@ -610,7 +627,7 @@ export function emitLexer(grammar: CstGrammar, st: LexerSymtab): string | null {
610627 emit ( ` }` ) ;
611628 emit ( ` throw new Error("Unexpected character at offset " + pos + ": '" + source[pos] + "'");` ) ;
612629 emit ( ` }` ) ;
613- emit ( ` if (wndHit >= 0) { tokN--; while (docLex.length > lexDiagBase && docLex[docLex.length - 1].offset >= tkOff[tokN]) docLex.length--; return wndHit; }` ) ;
630+ emit ( resyncRetractLine ( ' ' ) ) ;
614631 emit ( ` return hasMore ? -2 : -1;` ) ;
615632 emit ( `}` ) ;
616633 emit ( `// Windowed-relex restart anchor: the last token B ending at/before the damage` ) ;
0 commit comments