1717//
1818// Go/Rust toolchains are optional: a missing `go`/`rustc` is logged and skipped (the TS
1919// rendering, which needs only node, always runs).
20- import { execFileSync } from 'node:child_process' ;
20+ import { execFileSync , spawnSync } from 'node:child_process' ;
2121import { mkdirSync , rmSync , writeFileSync } from 'node:fs' ;
2222import { createParser } from '../src/gen-parser.ts' ;
23+ import { createLexer } from '../src/gen-lexer.ts' ;
2324import { emitParser , tsTarget , goTarget , rustTarget } from '../src/emit.ts' ;
2425import type { CstGrammar } from '../src/types.ts' ;
2526
@@ -270,21 +271,75 @@ function runProc(cmd: string, args: string[], src: string): Outcome {
270271 try { return { ok : true , cst : canon ( JSON . parse ( execFileSync ( cmd , args , { input : src , stdio : [ 'pipe' , 'pipe' , 'pipe' ] } ) . toString ( ) ) ) } ; }
271272 catch { return { ok : false } ; }
272273}
274+ type Align = { oldN : number ; newN : number ; prefix : number ; suffix : number } ;
275+ type EditOutcome = Outcome & { align ?: Align } ;
276+ function parseAlignStderr ( stderr : string ) : Align | undefined {
277+ for ( const line of stderr . split ( '\n' ) ) {
278+ const t = line . trim ( ) ;
279+ if ( ! t . startsWith ( '{' ) ) continue ;
280+ try { return JSON . parse ( t ) as Align ; } catch { /* keep scanning */ }
281+ }
282+ return undefined ;
283+ }
284+ function runEditSession ( cmd : string , args : string [ ] , json : string ) : EditOutcome {
285+ const r = spawnSync ( cmd , args , { input : json , encoding : 'utf8' } ) ;
286+ const align = parseAlignStderr ( r . stderr ?? '' ) ;
287+ if ( r . status !== 0 ) return { ok : false , align } ;
288+ try {
289+ return { ok : true , cst : canon ( JSON . parse ( r . stdout ) ) , align } ;
290+ } catch { return { ok : false , align } ; }
291+ }
292+
293+ type AlignMeta = { kind : string ; off : number ; end : number ; nl : boolean } ;
294+ const oracleToks = ( g : CstGrammar , src : string ) : AlignMeta [ ] => {
295+ const { tokenize } = createLexer ( g ) ;
296+ return tokenize ( src ) . map ( ( t ) => ( { kind : t . type , off : t . offset , end : t . offset + t . text . length , nl : t . newlineBefore } ) ) ;
297+ } ;
298+ const computeAlign = ( oldText : string , oldToks : AlignMeta [ ] , newText : string , newToks : AlignMeta [ ] ) : Align => {
299+ const oldN = oldToks . length , newN = newToks . length ;
300+ let prefix = 0 ;
301+ while ( prefix < oldN && prefix < newN ) {
302+ const o = oldToks [ prefix ] , n = newToks [ prefix ] ;
303+ if ( o . kind !== n . kind || o . off !== n . off || o . end !== n . end || o . nl !== n . nl ) break ;
304+ if ( oldText . slice ( o . off , o . end ) !== newText . slice ( n . off , n . end ) ) break ;
305+ prefix ++ ;
306+ }
307+ const delta = newText . length - oldText . length ;
308+ const minN = Math . min ( oldN , newN ) ;
309+ let suffix = 0 ;
310+ while ( prefix + suffix < minN ) {
311+ const o = oldToks [ oldN - 1 - suffix ] , n = newToks [ newN - 1 - suffix ] ;
312+ if ( o . kind !== n . kind || o . nl !== n . nl || n . off !== o . off + delta || n . end !== o . end + delta ) break ;
313+ if ( oldText . slice ( o . off , o . end ) !== newText . slice ( n . off , n . end ) ) break ;
314+ suffix ++ ;
315+ }
316+ return { oldN, newN, prefix, suffix } ;
317+ } ;
318+ const expectAlign = ( g : CstGrammar , init : string , batches : EditBatch [ ] ) : Align => {
319+ let text = init ;
320+ for ( let i = 0 ; i < batches . length - 1 ; i ++ ) for ( const [ s , e , r ] of batches [ i ] ) text = text . slice ( 0 , s ) + r + text . slice ( e ) ;
321+ const oldText = text ;
322+ const batch = batches [ batches . length - 1 ] ;
323+ for ( const [ s , e , r ] of batch ) text = text . slice ( 0 , s ) + r + text . slice ( e ) ;
324+ return computeAlign ( oldText , oracleToks ( g , oldText ) , text , oracleToks ( g , text ) ) ;
325+ } ;
273326
274327type EditBatch = [ number , number , string ] [ ] ;
275- type EditScenario = { init : string ; batches : EditBatch [ ] } ;
328+ type EditScenario = { init : string ; batches : EditBatch [ ] ; large ?: boolean } ;
276329const EDIT_SCENARIOS : Record < string , EditScenario [ ] > = {
277330 calc : [
278331 { init : '1+2*3' , batches : [ [ [ 3 , 3 , '4' ] ] ] } ,
279332 { init : '1+2*3' , batches : [ [ [ 1 , 3 , '' ] ] ] } ,
280333 { init : '1+2*3' , batches : [ [ [ 2 , 5 , '(7-8)' ] ] ] } ,
281334 { init : '1+2*3' , batches : [ [ [ 0 , 0 , '9-' ] ] , [ [ 7 , 8 , '' ] ] ] } ,
335+ { init : '1+2*3+' . repeat ( 199 ) + '1+2*3' , batches : [ [ [ 600 , 601 , '9' ] ] ] , large : true } ,
282336 ] ,
283337 javascript : [
284338 { init : 'let a = 1;\nf(a);' , batches : [ [ [ 8 , 9 , '42' ] ] ] } ,
285339 { init : 'let a = 1;\nf(a);' , batches : [ [ [ 11 , 16 , '' ] ] ] } ,
286340 { init : 'let a = 1;\nf(a);' , batches : [ [ [ 4 , 5 , 'b' ] , [ 12 , 13 , 'b' ] ] ] } ,
287341 { init : 'let a = 1;\nf(a);' , batches : [ [ [ 16 , 16 , '\ng(b);' ] ] , [ [ 0 , 4 , 'var' ] ] ] } ,
342+ { init : 'let a = 1;\nf(a);\n' . repeat ( 80 ) , batches : [ [ [ 688 , 689 , '9' ] ] ] , large : true } ,
288343 ] ,
289344} ;
290345const applyEdits = ( init : string , batches : EditBatch [ ] ) : string => {
@@ -358,12 +413,10 @@ for (const c of CASES) {
358413
359414 const editScenarios = EDIT_SCENARIOS [ c . grammar ] ;
360415 if ( editScenarios ) {
361- let editOk = 0 ;
362- const runEdit = ( json : string ) => runProc (
363- r . label === 'typescript' ? 'node' : r . label === 'go' ? `${ dir } /go/p` : `${ dir } /pr` ,
364- r . label === 'typescript' ? [ `${ dir } /p.ts` , 'edit-session' ] : [ 'edit-session' ] ,
365- json ,
366- ) ;
416+ let editOk = 0 , alignOk = 0 ;
417+ const editCmd = r . label === 'typescript' ? 'node' : r . label === 'go' ? `${ dir } /go/p` : `${ dir } /pr` ;
418+ const editArgs = r . label === 'typescript' ? [ `${ dir } /p.ts` , 'edit-session' ] : [ 'edit-session' ] ;
419+ const runEdit = ( json : string ) => runEditSession ( editCmd , editArgs , json ) ;
367420 for ( const sc of editScenarios ) {
368421 const final = applyEdits ( sc . init , sc . batches ) ;
369422 const a = runEdit ( JSON . stringify ( { init : sc . init , batches : sc . batches } ) ) ;
@@ -373,8 +426,20 @@ for (const c of CASES) {
373426 failures ++ ;
374427 console . log ( ` ${ c . grammar } /${ r . label } : edit-session mismatch (final=${ JSON . stringify ( final ) } ) A ok=${ a . ok } B ok=${ b . ok } ` ) ;
375428 }
429+ const want = expectAlign ( grammar , sc . init , sc . batches ) ;
430+ if ( a . align && a . align . oldN === want . oldN && a . align . newN === want . newN && a . align . prefix === want . prefix && a . align . suffix === want . suffix ) {
431+ alignOk ++ ;
432+ if ( sc . large && want . prefix + want . suffix < want . oldN - 8 ) {
433+ failures ++ ;
434+ console . log ( ` ${ c . grammar } /${ r . label } : large-doc align too narrow prefix+suffix=${ want . prefix + want . suffix } oldN=${ want . oldN } ` ) ;
435+ }
436+ } else {
437+ failures ++ ;
438+ console . log ( ` ${ c . grammar } /${ r . label } : token-align mismatch want=${ JSON . stringify ( want ) } got=${ JSON . stringify ( a . align ) } ` ) ;
439+ }
376440 }
377441 console . log ( ` ${ c . grammar } /${ r . label } : ${ editOk } /${ editScenarios . length } edit-sessions ≡ fresh` ) ;
442+ console . log ( ` ${ c . grammar } /${ r . label } : ${ alignOk } /${ editScenarios . length } token-alignments ≡ oracle` ) ;
378443 }
379444 }
380445}
0 commit comments