Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/target-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,21 @@ func parse(t []Tok) int32 {
\tnodes = nodes[:0]; kids = kids[:0]; scratch = scratch[:0]
\treturn parse${ir.entry}()
}

type Edit struct { Start, End int; Text string }
type Doc struct { text string; root int32 }
func NewDoc(src string) *Doc {
\td := &Doc{text: src}
\td.root = parse(tokenize(src))
\treturn d
}
func (d *Doc) Text() string { return d.text }
func (d *Doc) Root() int32 { return d.root }
func (d *Doc) Edit(edits []Edit) int32 {
\tfor _, e := range edits { d.text = d.text[:e.Start] + e.Text + d.text[e.End:] }
\td.root = parse(tokenize(d.text))
\treturn d.root
}
`;
},
emitRunner(): string {
Expand All @@ -559,6 +574,7 @@ func parse(t []Tok) int32 {
package main

import (
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -571,7 +587,7 @@ func main() {
\tdata, _ := io.ReadAll(os.Stdin)
\tsrc := string(data)
\t// Self-bench: a numeric arg N times the lex+parse loop and prints ms/iteration.
\tif len(os.Args) > 1 {
\tif len(os.Args) > 1 && os.Args[1] != "edit-session" {
\t\tif iters, err := strconv.Atoi(os.Args[1]); err == nil && iters > 0 {
\t\t\tfor i := 0; i < 3; i++ { parse(tokenize(src)) }
\t\t\tt0 := time.Now()
Expand All @@ -580,6 +596,27 @@ func main() {
\t\t\treturn
\t\t}
\t}
\tif len(os.Args) > 1 && os.Args[1] == "edit-session" {
\t\tvar sess struct {
\t\t\tInit string \`json:"init"\`
\t\t\tBatches [][][3]interface{} \`json:"batches"\`
\t\t}
\t\tif json.Unmarshal(data, &sess) != nil { os.Exit(1) }
\t\td := NewDoc(sess.Init)
\t\tfor _, batch := range sess.Batches {
\t\t\tedits := make([]Edit, len(batch))
\t\t\tfor i, t := range batch {
\t\t\t\tedits[i] = Edit{Start: int(t[0].(float64)), End: int(t[1].(float64)), Text: t[2].(string)}
\t\t\t}
\t\t\td.Edit(edits)
\t\t}
\t\troot := d.Root()
\t\tif root < 0 || pos != len(toks) { fmt.Fprintf(os.Stderr, "parse error (pos %d/%d)\\n", pos, len(toks)); os.Exit(1) }
\t\tvar b strings.Builder
\t\twriteJSON(root, &b)
\t\tos.Stdout.WriteString(b.String())
\t\treturn
\t}
\troot := parse(tokenize(src))
\tif root < 0 || pos != len(toks) {
\t\tfmt.Fprintf(os.Stderr, "parse error (pos %d/%d)\\n", pos, len(toks))
Expand Down
116 changes: 115 additions & 1 deletion src/target-rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,19 +546,133 @@ fn parse<'a>(tokens: Tokens<'a>) -> Option<(Parser<'a>, i32)> {
_ => None,
}
}

pub struct Edit { pub start: usize, pub end: usize, pub text: String }
pub struct Doc { text: String }
impl Doc {
pub fn new(text: String) -> Doc { Doc { text } }
pub fn text(&self) -> &str { &self.text }
pub fn edit(&mut self, edits: &[Edit]) {
for e in edits { let s = e.start; let en = e.end; self.text = format!("{}{}{}", &self.text[..s], e.text, &self.text[en..]); }
}
pub fn parse(&self) -> Option<(Parser<'_>, i32)> { parse(tokenize(&self.text)) }
}
`;
},
emitRunner(): string {
return `
// CLI runner (harness only): stdin -> CST JSON + a self-bench mode. Appended to the parser
// library by the gate (same file/crate, so it calls \`parse\`/\`write_json\` directly); NOT part
// of the parser.
fn skip_ws(s: &[u8], mut i: usize) -> usize { while i < s.len() && (s[i] as char).is_whitespace() { i += 1; } i }
fn parse_str(s: &[u8], mut i: usize) -> Option<(String, usize)> {
if s.get(i)? != &b'"' { return None; }
i += 1;
let mut out = String::new();
while i < s.len() {
match s[i] {
b'"' => return Some((out, i + 1)),
b'\\\\' => { i += 1; if i >= s.len() { return None; }
out.push(match s[i] { b'n' => '\\n', b'r' => '\\r', b't' => '\\t', b'"' => '"', b'\\\\' => '\\\\', b'/' => '/', c => c as char });
i += 1; }
c => { out.push(c as char); i += 1; }
}
}
None
}
fn parse_num(s: &[u8], mut i: usize) -> Option<(usize, usize)> {
let start = i;
while i < s.len() && s[i].is_ascii_digit() { i += 1; }
if i == start { return None; }
s[start..i].iter().fold(Some(0usize), |a, &d| a.and_then(|n| n.checked_mul(10).and_then(|m| m.checked_add((d - b'0') as usize))))
.map(|n| (n, i))
}
fn parse_triple(s: &[u8], mut i: usize) -> Option<((usize, usize, String), usize)> {
if s.get(i)? != &b'[' { return None; }
i = skip_ws(s, i + 1);
let (a, mut i) = parse_num(s, i)?;
i = skip_ws(s, i); if s.get(i)? != &b',' { return None; }
i = skip_ws(s, i + 1);
let (b, mut i) = parse_num(s, i)?;
i = skip_ws(s, i); if s.get(i)? != &b',' { return None; }
i = skip_ws(s, i + 1);
let (t, mut i) = parse_str(s, i)?;
i = skip_ws(s, i); if s.get(i)? != &b']' { return None; }
Some(((a, b, t), i + 1))
}
fn parse_batch(s: &[u8], mut i: usize) -> Option<(Vec<(usize, usize, String)>, usize)> {
if s.get(i)? != &b'[' { return None; }
i = skip_ws(s, i + 1);
let mut batch = Vec::new();
if s.get(i)? == &b']' { return Some((batch, i + 1)); }
loop {
let (t, ni) = parse_triple(s, i)?;
batch.push(t);
i = skip_ws(s, ni);
if s.get(i)? == &b']' { return Some((batch, i + 1)); }
if s.get(i)? != &b',' { return None; }
i = skip_ws(s, i + 1);
}
}
fn parse_edit_session(s: &str) -> Option<(String, Vec<Vec<(usize, usize, String)>>)> {
let b = s.as_bytes();
let mut i = skip_ws(b, 0);
if b.get(i)? != &b'{' { return None; }
i = skip_ws(b, i + 1);
let mut init = None;
let mut batches = None;
loop {
let (key, ni) = parse_str(b, i)?;
i = skip_ws(b, ni);
if b.get(i)? != &b':' { return None; }
i = skip_ws(b, i + 1);
if key == "init" {
let (v, ni) = parse_str(b, i)?;
init = Some(v);
i = skip_ws(b, ni);
} else if key == "batches" {
if b.get(i)? != &b'[' { return None; }
i = skip_ws(b, i + 1);
let mut bs = Vec::new();
if b.get(i)? == &b']' { batches = Some(bs); i += 1; }
else {
loop {
let (batch, ni) = parse_batch(b, i)?;
bs.push(batch);
i = skip_ws(b, ni);
if b.get(i)? == &b']' { batches = Some(bs); i += 1; break; }
if b.get(i)? != &b',' { return None; }
i = skip_ws(b, i + 1);
}
}
} else { return None; }
if b.get(i)? == &b'}' { break; }
if b.get(i)? != &b',' { return None; }
i = skip_ws(b, i + 1);
}
Some((init?, batches?))
}

fn main() {
use std::io::Read;
let mut src = String::new();
std::io::stdin().read_to_string(&mut src).unwrap();
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && args[1] == "edit-session" {
let (init, batches) = parse_edit_session(&src).unwrap();
let mut doc = Doc::new(init);
for batch in &batches {
let edits: Vec<Edit> = batch.iter().map(|&(s, e, ref t)| Edit { start: s, end: e, text: t.clone() }).collect();
doc.edit(&edits);
}
match doc.parse() {
Some((p, root)) => { let mut out = String::new(); write_json(&p, root, &mut out); print!("{}", out); }
None => { eprintln!("parse error"); std::process::exit(1); }
}
return;
}
// Self-bench: a numeric arg N times the lex+parse loop and prints ms/iteration.
if let Some(iters) = std::env::args().nth(1).and_then(|a| a.parse::<u64>().ok()) {
if let Some(iters) = args.get(1).and_then(|a| a.parse::<u64>().ok()) {
for _ in 0..3 { let s = std::hint::black_box(&src); if let Some((p, r)) = parse(tokenize(s)) { std::hint::black_box((&p.nodes[r as usize], p.pos)); } }
let t = std::time::Instant::now();
for _ in 0..iters { let s = std::hint::black_box(&src); if let Some((p, r)) = parse(tokenize(s)) { std::hint::black_box((&p.nodes[r as usize], p.pos)); } }
Expand Down
31 changes: 28 additions & 3 deletions src/target-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,15 +471,40 @@ export function parse(tokens: Tok[]): Cst | null {
const root = parse${ir.entry}();
return root !== null && pos === toks.length ? root : null;
}

export type Edit = { start: number; end: number; text: string };
export function createDoc(src: string): { text(): string; root(): Node | null; edit(edits: Edit[]): Node | null } {
let text = src;
let root: Node | null = parse(tokenize(src));
return {
text(): string { return text; },
root(): Node | null { return root; },
edit(edits: Edit[]): Node | null {
for (const e of edits) text = text.slice(0, e.start) + e.text + text.slice(e.end);
root = parse(tokenize(text));
return root;
},
};
}
`;
},
emitRunner(): string {
return `// CLI runner (harness only): stdin → CST JSON. Appended to the parser library by the gate;
// NOT part of the emitted parser. The import is hoisted, so it may follow the library code.
import { readFileSync } from 'node:fs';
const _root = parse(tokenize(readFileSync(0, 'utf8')));
if (_root === null) { process.stderr.write('parse error\\n'); process.exit(1); }
process.stdout.write(JSON.stringify(_root));
const _raw = readFileSync(0, 'utf8');
if (process.argv.includes('edit-session')) {
const { init, batches } = JSON.parse(_raw) as { init: string; batches: [number, number, string][][] };
const doc = createDoc(init);
for (const batch of batches) doc.edit(batch.map(([start, end, text]) => ({ start, end, text })));
const root = doc.root();
if (root === null) { process.stderr.write('parse error\\n'); process.exit(1); }
process.stdout.write(JSON.stringify(root));
} else {
const _root = parse(tokenize(_raw));
if (_root === null) { process.stderr.write('parse error\\n'); process.exit(1); }
process.stdout.write(JSON.stringify(_root));
}
`;
},
};
43 changes: 43 additions & 0 deletions test/portable-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,28 @@ function runProc(cmd: string, args: string[], src: string): Outcome {
catch { return { ok: false }; }
}

type EditBatch = [number, number, string][];
type EditScenario = { init: string; batches: EditBatch[] };
const EDIT_SCENARIOS: Record<string, EditScenario[]> = {
calc: [
{ init: '1+2*3', batches: [[[3, 3, '4']]] },
{ init: '1+2*3', batches: [[[1, 3, '']]] },
{ init: '1+2*3', batches: [[[2, 5, '(7-8)']]] },
{ init: '1+2*3', batches: [[[0, 0, '9-']], [[7, 8, '']]] },
],
javascript: [
{ init: 'let a = 1;\nf(a);', batches: [[[8, 9, '42']]] },
{ init: 'let a = 1;\nf(a);', batches: [[[11, 16, '']]] },
{ init: 'let a = 1;\nf(a);', batches: [[[4, 5, 'b'], [12, 13, 'b']]] },
{ init: 'let a = 1;\nf(a);', batches: [[[16, 16, '\ng(b);']], [[0, 4, 'var']]] },
],
};
const applyEdits = (init: string, batches: EditBatch[]): string => {
let text = init;
for (const batch of batches) for (const [start, end, repl] of batch) text = text.slice(0, start) + repl + text.slice(end);
return text;
};

let failures = 0;
for (const c of CASES) {
const grammar: CstGrammar = (await import(c.path)).default;
Expand Down Expand Up @@ -333,6 +355,27 @@ for (const c of CASES) {
console.log(` ${c.grammar}/${r.label}: REJECT mismatch on ${JSON.stringify(src)} (oracle ok=${want.ok}, ${r.label} ok=${got.ok})`);
}
console.log(` ${c.grammar}/${r.label}: ${acc}/${c.accept.length} accept ≡ oracle · ${rej}/${c.reject.length} reject ≡ oracle${snap ? ` · ${snap} shape drift` : ''}`);

const editScenarios = EDIT_SCENARIOS[c.grammar];
if (editScenarios) {
let editOk = 0;
const runEdit = (json: string) => runProc(
r.label === 'typescript' ? 'node' : r.label === 'go' ? `${dir}/go/p` : `${dir}/pr`,
r.label === 'typescript' ? [`${dir}/p.ts`, 'edit-session'] : ['edit-session'],
json,
);
for (const sc of editScenarios) {
const final = applyEdits(sc.init, sc.batches);
const a = runEdit(JSON.stringify({ init: sc.init, batches: sc.batches }));
const b = r.run(final);
if (a.ok === b.ok && (!a.ok || a.cst === b.cst) && a.ok === oracleOut(final).ok) editOk++;
else {
failures++;
console.log(` ${c.grammar}/${r.label}: edit-session mismatch (final=${JSON.stringify(final)}) A ok=${a.ok} B ok=${b.ok}`);
}
}
console.log(` ${c.grammar}/${r.label}: ${editOk}/${editScenarios.length} edit-sessions ≡ fresh`);
}
}
}

Expand Down
Loading