Skip to content

Commit 550b8ff

Browse files
committed
Add S2 token alignment with safe edit bounds and large-doc gate.
1 parent 5181178 commit 550b8ff

4 files changed

Lines changed: 224 additions & 19 deletions

File tree

src/target-go.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,17 +552,65 @@ func parse(t []Tok) int32 {
552552
}
553553
554554
type Edit struct { Start, End int; Text string }
555-
type Doc struct { text string; root int32 }
555+
type alignMeta struct { Kind string; Off, End int; Nl bool }
556+
type Align struct {
557+
\tOldN int \`json:"oldN"\`
558+
\tNewN int \`json:"newN"\`
559+
\tPrefix int \`json:"prefix"\`
560+
\tSuffix int \`json:"suffix"\`
561+
}
562+
func toMeta(toks []Tok) []alignMeta {
563+
\tm := make([]alignMeta, len(toks))
564+
\tfor i, t := range toks { m[i] = alignMeta{t.Kind, t.Off, t.End, t.Nl} }
565+
\treturn m
566+
}
567+
func computeAlign(oldText string, oldToks []alignMeta, newText string, newToks []alignMeta) Align {
568+
\toldN, newN := len(oldToks), len(newToks)
569+
\tprefix := 0
570+
\tfor prefix < oldN && prefix < newN {
571+
\t\to, n := oldToks[prefix], newToks[prefix]
572+
\t\tif o.Kind != n.Kind || o.Off != n.Off || o.End != n.End || o.Nl != n.Nl { break }
573+
\t\tif oldText[o.Off:o.End] != newText[n.Off:n.End] { break }
574+
\t\tprefix++
575+
\t}
576+
\tdelta := len(newText) - len(oldText)
577+
\tminN := oldN; if newN < minN { minN = newN }
578+
\tsuffix := 0
579+
\tfor prefix+suffix < minN {
580+
\t\to, n := oldToks[oldN-1-suffix], newToks[newN-1-suffix]
581+
\t\tif o.Kind != n.Kind || o.Nl != n.Nl || n.Off != o.Off+delta || n.End != o.End+delta { break }
582+
\t\tif oldText[o.Off:o.End] != newText[n.Off:n.End] { break }
583+
\t\tsuffix++
584+
\t}
585+
\treturn Align{oldN, newN, prefix, suffix}
586+
}
587+
type Doc struct { text string; root int32; toks []alignMeta; align *Align }
556588
func NewDoc(src string) *Doc {
557589
\td := &Doc{text: src}
590+
\td.toks = toMeta(tokenize(src))
558591
\td.root = parse(tokenize(src))
559592
\treturn d
560593
}
561594
func (d *Doc) Text() string { return d.text }
562595
func (d *Doc) Root() int32 { return d.root }
596+
func (d *Doc) Align() *Align { return d.align }
597+
func applyEdit(text string, e Edit) string {
598+
\tn := len(text)
599+
\tstart, end := e.Start, e.End
600+
\tif start < 0 { start = 0 }
601+
\tif start > n { start = n }
602+
\tif end < start { end = start }
603+
\tif end > n { end = n }
604+
\treturn text[:start] + e.Text + text[end:]
605+
}
563606
func (d *Doc) Edit(edits []Edit) int32 {
564-
\tfor _, e := range edits { d.text = d.text[:e.Start] + e.Text + d.text[e.End:] }
565-
\td.root = parse(tokenize(d.text))
607+
\toldText, oldToks := d.text, d.toks
608+
\tfor _, e := range edits { d.text = applyEdit(d.text, e) }
609+
\tnewToks := tokenize(d.text)
610+
\td.toks = toMeta(newToks)
611+
\ta := computeAlign(oldText, oldToks, d.text, d.toks)
612+
\td.align = &a
613+
\td.root = parse(newToks)
566614
\treturn d.root
567615
}
568616
`;
@@ -610,6 +658,7 @@ func main() {
610658
\t\t\t}
611659
\t\t\td.Edit(edits)
612660
\t\t}
661+
\t\tif a := d.Align(); a != nil { b, _ := json.Marshal(a); os.Stderr.Write(append(b, '\\n')) }
613662
\t\troot := d.Root()
614663
\t\tif root < 0 || pos != len(toks) { fmt.Fprintf(os.Stderr, "parse error (pos %d/%d)\\n", pos, len(toks)); os.Exit(1) }
615664
\t\tvar b strings.Builder

src/target-rust.ts

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -548,14 +548,65 @@ fn parse<'a>(tokens: Tokens<'a>) -> Option<(Parser<'a>, i32)> {
548548
}
549549
550550
pub struct Edit { pub start: usize, pub end: usize, pub text: String }
551-
pub struct Doc { text: String }
551+
#[derive(Clone)]
552+
struct AlignMeta { kind: &'static str, off: usize, end: usize, nl: bool }
553+
struct Align { old_n: usize, new_n: usize, prefix: usize, suffix: usize }
554+
fn to_meta(toks: &[Tok<'_>]) -> Vec<AlignMeta> {
555+
toks.iter().map(|t| AlignMeta { kind: t.kind, off: t.off, end: t.end, nl: t.nl }).collect()
556+
}
557+
fn compute_align(old_text: &str, old_toks: &[AlignMeta], new_text: &str, new_toks: &[AlignMeta]) -> Align {
558+
let old_n = old_toks.len();
559+
let new_n = new_toks.len();
560+
let mut prefix = 0usize;
561+
while prefix < old_n && prefix < new_n {
562+
let o = &old_toks[prefix];
563+
let n = &new_toks[prefix];
564+
if o.kind != n.kind || o.off != n.off || o.end != n.end || o.nl != n.nl { break; }
565+
if old_text[o.off..o.end] != new_text[n.off..n.end] { break; }
566+
prefix += 1;
567+
}
568+
let delta = new_text.len() as isize - old_text.len() as isize;
569+
let min_n = old_n.min(new_n);
570+
let mut suffix = 0usize;
571+
while prefix + suffix < min_n {
572+
let o = &old_toks[old_n - 1 - suffix];
573+
let n = &new_toks[new_n - 1 - suffix];
574+
if o.kind != n.kind || o.nl != n.nl { break; }
575+
if n.off != (o.off as isize + delta) as usize || n.end != (o.end as isize + delta) as usize { break; }
576+
if old_text[o.off..o.end] != new_text[n.off..n.end] { break; }
577+
suffix += 1;
578+
}
579+
Align { old_n, new_n, prefix, suffix }
580+
}
581+
fn toks_from_meta<'a>(text: &'a str, meta: &[AlignMeta]) -> Vec<Tok<'a>> {
582+
meta.iter().map(|m| Tok { kind: m.kind, text: &text[m.off..m.end], off: m.off, end: m.end, nl: m.nl }).collect()
583+
}
584+
pub struct Doc { text: String, toks: Vec<AlignMeta>, align: Option<Align> }
552585
impl Doc {
553-
pub fn new(text: String) -> Doc { Doc { text } }
586+
pub fn new(text: String) -> Doc { Doc { text: text.clone(), toks: to_meta(&lex(&text)), align: None } }
554587
pub fn text(&self) -> &str { &self.text }
588+
pub fn alignment(&self) -> Option<&Align> { self.align.as_ref() }
555589
pub fn edit(&mut self, edits: &[Edit]) {
556-
for e in edits { let s = e.start; let en = e.end; self.text = format!("{}{}{}", &self.text[..s], e.text, &self.text[en..]); }
590+
let old_text = self.text.clone();
591+
let old_toks = self.toks.clone();
592+
for e in edits {
593+
let n = self.text.len();
594+
let start = e.start.min(n);
595+
let end = e.end.max(start).min(n);
596+
self.text = format!("{}{}{}", &self.text[..start], e.text, &self.text[end..]);
597+
}
598+
self.toks = to_meta(&lex(&self.text));
599+
self.align = Some(compute_align(&old_text, &old_toks, &self.text, &self.toks));
600+
}
601+
pub fn parse(&self) -> Option<(Parser<'_>, i32)> {
602+
let toks = toks_from_meta(&self.text, &self.toks);
603+
let n = toks.len();
604+
let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new(), suppress_cur: Vec::new(), src: &self.text, nodes: Vec::new(), kids: Vec::new(), scratch: Vec::new() };
605+
match p.parse_${ir.entry}() {
606+
Some(root) if p.pos == n => Some((p, root)),
607+
_ => None,
608+
}
557609
}
558-
pub fn parse(&self) -> Option<(Parser<'_>, i32)> { parse(tokenize(&self.text)) }
559610
}
560611
`;
561612
},
@@ -665,8 +716,13 @@ fn main() {
665716
let edits: Vec<Edit> = batch.iter().map(|&(s, e, ref t)| Edit { start: s, end: e, text: t.clone() }).collect();
666717
doc.edit(&edits);
667718
}
719+
if let Some(a) = doc.alignment() {
720+
eprintln!("{{\\"oldN\\":{},\\"newN\\":{},\\"prefix\\":{},\\"suffix\\":{}}}", a.old_n, a.new_n, a.prefix, a.suffix);
721+
}
668722
match doc.parse() {
669-
Some((p, root)) => { let mut out = String::new(); write_json(&p, root, &mut out); print!("{}", out); }
723+
Some((p, root)) => {
724+
let mut out = String::new(); write_json(&p, root, &mut out); print!("{}", out);
725+
}
670726
None => { eprintln!("parse error"); std::process::exit(1); }
671727
}
672728
return;

src/target-ts.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,15 +473,48 @@ export function parse(tokens: Tok[]): Cst | null {
473473
}
474474
475475
export type Edit = { start: number; end: number; text: string };
476-
export function createDoc(src: string): { text(): string; root(): Node | null; edit(edits: Edit[]): Node | null } {
476+
type AlignMeta = { kind: string; off: number; end: number; nl: boolean };
477+
type Align = { oldN: number; newN: number; prefix: number; suffix: number };
478+
const toMeta = (toks: Tok[]): AlignMeta[] => toks.map((t) => ({ kind: t.kind, off: t.off, end: t.end, nl: t.nl }));
479+
function computeAlign(oldText: string, oldToks: AlignMeta[], newText: string, newToks: AlignMeta[]): Align {
480+
const oldN = oldToks.length, newN = newToks.length;
481+
let prefix = 0;
482+
while (prefix < oldN && prefix < newN) {
483+
const o = oldToks[prefix], n = newToks[prefix];
484+
if (o.kind !== n.kind || o.off !== n.off || o.end !== n.end || o.nl !== n.nl) break;
485+
if (oldText.slice(o.off, o.end) !== newText.slice(n.off, n.end)) break;
486+
prefix++;
487+
}
488+
const delta = newText.length - oldText.length;
489+
const minN = Math.min(oldN, newN);
490+
let suffix = 0;
491+
while (prefix + suffix < minN) {
492+
const o = oldToks[oldN - 1 - suffix], n = newToks[newN - 1 - suffix];
493+
if (o.kind !== n.kind || o.nl !== n.nl || n.off !== o.off + delta || n.end !== o.end + delta) break;
494+
if (oldText.slice(o.off, o.end) !== newText.slice(n.off, n.end)) break;
495+
suffix++;
496+
}
497+
return { oldN, newN, prefix, suffix };
498+
}
499+
export function createDoc(src: string): { text(): string; root(): Node | null; align(): Align | null; edit(edits: Edit[]): Node | null } {
477500
let text = src;
501+
let prevToks = toMeta(tokenize(src));
502+
let align: Align | null = null;
478503
let root: Node | null = parse(tokenize(src));
479504
return {
480505
text(): string { return text; },
481506
root(): Node | null { return root; },
507+
align(): Align | null { return align; },
482508
edit(edits: Edit[]): Node | null {
483-
for (const e of edits) text = text.slice(0, e.start) + e.text + text.slice(e.end);
484-
root = parse(tokenize(text));
509+
const oldText = text, oldToks = prevToks;
510+
for (const e of edits) {
511+
const n = text.length, start = Math.max(0, Math.min(e.start, n)), end = Math.max(start, Math.min(e.end, n));
512+
text = text.slice(0, start) + e.text + text.slice(end);
513+
}
514+
const newToks = tokenize(text);
515+
prevToks = toMeta(newToks);
516+
align = computeAlign(oldText, oldToks, text, prevToks);
517+
root = parse(newToks);
485518
return root;
486519
},
487520
};
@@ -497,6 +530,8 @@ if (process.argv.includes('edit-session')) {
497530
const { init, batches } = JSON.parse(_raw) as { init: string; batches: [number, number, string][][] };
498531
const doc = createDoc(init);
499532
for (const batch of batches) doc.edit(batch.map(([start, end, text]) => ({ start, end, text })));
533+
const a = doc.align();
534+
if (a) process.stderr.write(JSON.stringify(a) + '\\n');
500535
const root = doc.root();
501536
if (root === null) { process.stderr.write('parse error\\n'); process.exit(1); }
502537
process.stdout.write(JSON.stringify(root));

test/portable-targets.ts

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
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';
2121
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2222
import { createParser } from '../src/gen-parser.ts';
23+
import { createLexer } from '../src/gen-lexer.ts';
2324
import { emitParser, tsTarget, goTarget, rustTarget } from '../src/emit.ts';
2425
import 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

274327
type EditBatch = [number, number, string][];
275-
type EditScenario = { init: string; batches: EditBatch[] };
328+
type EditScenario = { init: string; batches: EditBatch[]; large?: boolean };
276329
const 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
};
290345
const 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

Comments
 (0)