You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
emit: separate the parser library from the CLI runner (Target ≠ CLI)
The portable targets baked a stdin → CST-JSON `main` into the emitted source — harness
scaffolding the gate needs to execute and diff a compiled go/rust binary, but not part
of the parser. So `emitParser(grammar, tsTarget|goTarget|rustTarget)` shipped a CLI app,
not a parser; the consumption model also diverged from jsTarget (an importable library).
Split the two:
- `emitParser(grammar, target)` now emits a parser LIBRARY only — a `parse` entry, no I/O,
no `main`. All four targets are consistent: you import/embed and call `parse`.
- `target.emitRunner()` (new, optional on Target) emits the stdin → JSON CLI wrapper. The
gate assembles library + runner itself: ts/rust append it (one file); Go writes it as a
separate runner.go in the same `package main` (Go forbids a second import block after
declarations), so the parser file keeps only its own imports.
Net: `emitParser(grammar, tsTarget)` is now directly `import { parse }` → a plain CST tree
— the "import a module, get a tree" path that was previously missing (jsTarget is a
low-level arena API; the portable TS gives a materialized CST). The CLI is the harness's
concern, where it belongs.
portable-targets 16 grammars ×3 (library + runner, compiled & run), full suite 42/42.
README updated to show library usage.
Copy file name to clipboardExpand all lines: README.md
+7-4Lines changed: 7 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -340,15 +340,18 @@ const Regex = token(seq(
340
340
341
341
### The emitted parser need not be JS — Go, Rust, native
342
342
343
-
The grammar also derives a **standalone parser in another language**. [`emitParser(grammar, target)`](src/emit.ts) runs one analysis into one language-agnostic IR, and each `Target` renders it — including its own regex-free lexer (`emitParser` reuses `emitLexer(grammar, target)`), so the output has no dependency on the JS runtime and compiles offline:
343
+
The grammar also derives a **parser library in another language**. [`emitParser(grammar, target)`](src/emit.ts) runs one analysis into one language-agnostic IR, and each `Target` renders it — including its own regex-free lexer (`emitParser` reuses `emitLexer(grammar, target)`), so the output has no dependency on the JS runtime and compiles offline. What it emits is a **library** — a `parse` entry, no I/O — that you embed and call:
writeFileSync('parser.go', emitParser(grammar, goTarget)); // a Go parser, no deps
350
+
writeFileSync('parser.rs', emitParser(grammar, rustTarget)); // a Rust parser, no crates
350
351
```
351
352
353
+
The CLI shape `test/portable-targets.ts` runs (stdin → CST JSON) is a *harness* wrapper — `target.emitRunner()`, appended by the gate to make the library executable — not part of the parser.
354
+
352
355
The proof is the full languages: the real [`javascript.ts`](javascript.ts) and [`typescript.ts`](typescript.ts) grammars — including the `[Await]/[Yield]` fork, left recursion, the regex/division and template state machines, arrow functions, and the TS type grammar — emit to **TypeScript, Go, and Rust**, and every CST is byte-identical to the reference interpreter. [`test/portable-targets.ts`](test/portable-targets.ts) compiles and runs all three for sixteen grammars (the two real languages plus focused fixtures) on every CI run. The Rust output reaches [oxc](https://github.com/oxc-project/oxc) throughput and the Go output beats [tsgo](https://github.com/microsoft/typescript-go) on the same corpus (an arena keeps both near zero-allocation). Byte-based Go/Rust use UTF-8 offsets — identical to the JS interpreter's for ASCII; non-ASCII offset units differ inherently.
// Self-bench: a numeric arg N times the lex+parse loop and prints ms/iteration.
422
441
if let Some(iters) = std::env::args().nth(1).and_then(|a| a.parse::<u64>().ok()) {
423
-
// black_box on the input + result so the optimizer can't elide the lex/parse.
424
-
for _ in 0..3 { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new(), src: &src }; std::hint::black_box(p.parse_${ir.entry}()); }
442
+
for _ in 0..3 { std::hint::black_box(parse(std::hint::black_box(&src))); }
425
443
let t = std::time::Instant::now();
426
-
for _ in 0..iters { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new(), src: &src }; std::hint::black_box(p.parse_${ir.entry}()); }
444
+
for _ in 0..iters { std::hint::black_box(parse(std::hint::black_box(&src))); }
427
445
println!("{:.4}", t.elapsed().as_secs_f64() * 1000.0 / iters as f64);
428
446
return;
429
447
}
430
-
let toks = lex(&src);
431
-
let n = toks.len();
432
-
let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new(), src: &src };
433
-
match p.parse_${ir.entry}() {
434
-
Some(root) if p.pos == n => { let mut out = String::new(); write_json(&root, &mut out); print!("{}", out); }
0 commit comments