Skip to content

Commit cd4ebc8

Browse files
committed
emit-portable: typescript.ts emits too — both real grammars in the gate (issue #6)
The second real full language now goes through the agnostic emitter end-to-end. Two type-grammar constructs were the wall: - A LED with a leading `sameLine` guard (`$ sameLine '<' …`) — TS's generic-args / array / non-null type tails that must not cross a newline. The guard is hoisted into the led-arm condition (skip, don't break, so the connector can rebind). - `notLeftLeaf`: a led skipped when the LEFT node's head-leaf text is in a word set (`void`/`null`/`this` can't be `.`-qualified as a type). Each target gains a `headLeafText` (the leftmost leaf's source text) and the led arm checks it. typescript.ts (the most complex grammar) emits, compiles and runs in ts/go/rust, and is gate-maintained alongside javascript.ts (13/13 accept, 4/4 reject ×3, ASCII corpus; 83.5% on the broad curated TS corpus in TS). Full suite 42/42. The agnostic emitter now covers both full real languages — the issue-#6 goal, proven in three target languages.
1 parent 0b6d7fd commit cd4ebc8

5 files changed

Lines changed: 64 additions & 15 deletions

File tree

src/emit-portable.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export type PrattRule = {
7474
leds: Bracket[]; // LED: mixfix continuation (call/member/index), tried before operators
7575
ledAccessTail: boolean[]; // parallel to leds: a "closed punct-connector" tail (member/call/index) — disabled once a postfix binds
7676
ledLbp: Array<number | null>; // parallel to leds: precedence gate (ternary/in/instanceof) — bind only when lbp > minBp; null = bind maximally tight
77+
ledSameLine: boolean[]; // parallel to leds: a leading `sameLine` guard (TS type tails) — the connector must be on the operand's line
78+
ledNotLeftLeaf: Array<string[] | null>; // parallel to leds: skip this led when the left node's head-leaf text is in this set (`void.x` etc.)
7779
postfixToks: string[]; // LED: a postfix token `$ X` (e.g. a tagged template), tried like a mixfix led (also an access tail)
7880
postfix: Array<{ op: string; lbp: number }>; // LED: a postfix operator `$ ++` — binds iff lbp > minBp + !tailClosed, no rhs, closes the tail
7981
};
@@ -304,9 +306,17 @@ function buildPratt(
304306
const leds: Bracket[] = [];
305307
const ledAccessTail: boolean[] = [];
306308
const ledLbp: Array<number | null> = [];
309+
const ledSameLine: boolean[] = [];
310+
const ledNotLeftLeaf: Array<string[] | null> = [];
307311
const postfixToks: string[] = [];
308312
for (const alt of alts) {
309-
const items = alt.type === 'seq' ? alt.items : [alt];
313+
let items = alt.type === 'seq' ? alt.items : [alt];
314+
// A left-recursive continuation may carry a leading `notLeftLeaf(words)` head-leaf guard
315+
// before the self `$` — strip it and attach the word set to the led it produces.
316+
let nllWords: string[] | null = null;
317+
if (items[0].type === 'notLeftLeaf' && items[1]?.type === 'ref' && items[1].name === name) {
318+
nllWords = items[0].words; items = items.slice(1);
319+
}
310320
const startsSelf = items[0].type === 'ref' && items[0].name === name;
311321
if (!startsSelf) {
312322
// NUD
@@ -331,9 +341,11 @@ function buildPratt(
331341
continue;
332342
}
333343
// LED (starts with self): `$ op $` (binary, op slot + trailing self) or `$ <lit> …` (mixfix)
334-
const rest = items.slice(1);
335-
if (rest[0].type === 'op') { sawBinary = true; continue; }
336-
if (rest[0].type === 'postfix') { sawPostfix = true; continue; } // postfix operator (`x++`)
344+
const restAll = items.slice(1);
345+
const hasSameLine = restAll[0]?.type === 'sameLine'; // a TS type tail: `$ sameLine '<' …`
346+
const rest = hasSameLine ? restAll.slice(1) : restAll;
347+
if (!hasSameLine && rest[0].type === 'op') { sawBinary = true; continue; }
348+
if (!hasSameLine && rest[0].type === 'postfix') { sawPostfix = true; continue; } // postfix operator (`x++`)
337349
if (rest[0].type === 'literal') {
338350
const conn = rest[0].value;
339351
const prec = a.ledPrecByConnector.get(conn); // { lbp, rhsBp } for ternary/in/instanceof
@@ -346,6 +358,8 @@ function buildPratt(
346358
leds.push({ first: conn, steps });
347359
ledAccessTail.push(!lastIsOperand && !wordConnector);
348360
ledLbp.push(prec ? prec.lbp : null);
361+
ledSameLine.push(hasSameLine);
362+
ledNotLeftLeaf.push(nllWords);
349363
continue;
350364
}
351365
if (rest.length === 1 && rest[0].type === 'ref' && a.tokenNames.has(rest[0].name)) { postfixToks.push(rest[0].name); continue; } // postfix token (tagged template)
@@ -376,5 +390,5 @@ function buildPratt(
376390
const postfix = sawPostfix
377391
? [...a.opTable.entries()].filter(([, info]) => info.position === 'postfix').map(([op, info]) => ({ op, lbp: info.lbp }))
378392
: [];
379-
return { kind: 'pratt', name, cstName, nudToks, nudBrackets, nudSeqs, nudCapped, prefix, binary, leds, ledAccessTail, ledLbp, postfixToks, postfix };
393+
return { kind: 'pratt', name, cstName, nudToks, nudBrackets, nudSeqs, nudCapped, prefix, binary, leds, ledAccessTail, ledLbp, ledSameLine, ledNotLeftLeaf, postfixToks, postfix };
380394
}

src/target-go.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
210210
\t\tif ${b.steps.map(stepCond).join(' && ')} { return finish(${J(r.cstName)}, sb, t.Off) }
211211
\t\tpos = save; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]
212212
\t}`;
213-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}!_mySup[${J(b.first)}] && t.Text == ${J(b.first)} {
213+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null, sameLine: boolean, nll: string[] | null) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}${sameLine ? '!t.Nl && ' : ''}${nll ? `!_inW([]string{${nll.map(J).join(', ')}}, headLeafText(left)) && ` : ''}!_mySup[${J(b.first)}] && t.Text == ${J(b.first)} {
214214
\t\t\tledSave := pos; sb := len(scratch); nb := len(nodes); kb := len(kids)
215215
\t\t\tscratch = append(scratch, left)
216216
\t\t\tif ${b.steps.map(stepCond).join(' && ')} { left = finish(${J(r.cstName)}, sb, nodes[left].Offset); continue }
@@ -242,7 +242,7 @@ func ${r.name}bp(minBp int) int32 {
242242
\tfor {
243243
\t\tt := peek()
244244
\t\tif t == nil { break }
245-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
245+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i], r.ledSameLine[i], r.ledNotLeftLeaf[i])).join('\n')}
246246
${r.postfixToks.map(postfixArm).join('\n')}
247247
\t\tif post, ok := ${r.name}POST[t.Text]; ok && !tailClosed && post > minBp {
248248
\t\t\tsb := len(scratch); scratch = append(scratch, left, mkLeaf("$operator", t.Off, t.End)); pos++; tailClosed = true
@@ -338,6 +338,7 @@ type bp struct{ lbp, rbp int }
338338
var toks []Tok
339339
var pos int
340340
var _capped bool
341+
var _src string
341342
var _suppressNext map[string]bool
342343
var nodes []Node
343344
var kids []int32
@@ -412,7 +413,14 @@ func writeJSON(id int32, b *strings.Builder) {
412413
\tfmt.Fprintf(b, "],\\"offset\\":%d,\\"end\\":%d}", nd.Offset, nd.End)
413414
}
414415
416+
func headLeafText(id int32) string {
417+
\tfor !nodes[id].IsLeaf && nodes[id].KidCount > 0 { id = kids[nodes[id].KidStart] }
418+
\treturn _src[nodes[id].Offset:nodes[id].End]
419+
}
420+
func _inW(ws []string, s string) bool { for _, w := range ws { if w == s { return true } }; return false }
421+
415422
func parseOnce(src string) int32 {
423+
\t_src = src
416424
\ttoks = lex(src)
417425
\tpos = 0
418426
\tnodes = nodes[:0]; kids = kids[:0]; scratch = scratch[:0]

src/target-rust.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
241241
if ${b.steps.map(stepCond).join(' && ')} { return Some(node(${J(r.cstName)}, kids)); }
242242
self.pos = save; // fall through to the next NUD alternative
243243
}`;
244-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if ${accessTail ? '!tail_closed && ' : ''}${lbp !== null ? `${lbp} > min_bp && ` : ''}!my_sup.iter().any(|c| *c == ${J(b.first)}) && t.text == ${J(b.first)} {
244+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null, sameLine: boolean, nll: string[] | null) => ` if ${accessTail ? '!tail_closed && ' : ''}${lbp !== null ? `${lbp} > min_bp && ` : ''}${sameLine ? '!t.nl && ' : ''}${nll ? `!self.nll_blocked(&[${nll.map(J).join(', ')}], &left) && ` : ''}!my_sup.iter().any(|c| *c == ${J(b.first)}) && t.text == ${J(b.first)} {
245245
let led_save = self.pos; let mut kids: Vec<Cst> = Vec::new();
246246
if ${b.steps.map(stepCond).join(' && ')} {
247247
let mut full = vec![left]; full.append(&mut kids);
@@ -268,7 +268,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
268268
let mut tail_closed = false;
269269
loop {
270270
let t = match self.peek() { Some(t) => t, None => break };
271-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
271+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i], r.ledSameLine[i], r.ledNotLeftLeaf[i])).join('\n')}
272272
${r.postfixToks.map(postfixArm).join('\n')}
273273
if let Some(plbp) = Parser::${r.name}_post(t.text) { if !tail_closed && plbp > min_bp { self.pos += 1; let op_leaf = Cst::leaf("$operator", t.off, t.end); left = node(${J(r.cstName)}, vec![left, op_leaf]); tail_closed = true; continue; } }
274274
let (lbp, rbp) = match Parser::${r.name}_bin(t.text) { Some(x) => x, None => break };
@@ -352,9 +352,15 @@ fn node(rule: &'static str, kids: Vec<Cst>) -> Cst { let o = kids[0].offset; let
352352
353353
${lexer(ir)}
354354
355-
struct Parser<'a> { toks: Vec<Tok<'a>>, pos: usize, capped: bool, suppress_next: Vec<&'static str> }
355+
struct Parser<'a> { toks: Vec<Tok<'a>>, pos: usize, capped: bool, suppress_next: Vec<&'static str>, src: &'a str }
356356
impl<'a> Parser<'a> {
357357
fn peek(&self) -> Option<Tok<'a>> { if self.pos < self.toks.len() { Some(self.toks[self.pos]) } else { None } }
358+
fn head_leaf_text(&self, node: &Cst) -> &'a str {
359+
let mut n = node;
360+
while !n.children.is_empty() { n = &n.children[0]; }
361+
&self.src[n.offset..n.end]
362+
}
363+
fn nll_blocked(&self, words: &[&str], node: &Cst) -> bool { let h = self.head_leaf_text(node); words.iter().any(|w| *w == h) }
358364
fn branch(&self, rule: &'static str, kids: Vec<Cst>, save: usize) -> Cst {
359365
let offset = if !kids.is_empty() { kids[0].offset } else if save < self.toks.len() { self.toks[save].off } else { 0 };
360366
let end = if !kids.is_empty() { kids[kids.len() - 1].end } else { offset };
@@ -409,15 +415,15 @@ fn main() {
409415
// Self-bench: a numeric arg N times the lex+parse loop and prints ms/iteration.
410416
if let Some(iters) = std::env::args().nth(1).and_then(|a| a.parse::<u64>().ok()) {
411417
// black_box on the input + result so the optimizer can't elide the lex/parse.
412-
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() }; std::hint::black_box(p.parse_${ir.entry}()); }
418+
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}()); }
413419
let t = std::time::Instant::now();
414-
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() }; std::hint::black_box(p.parse_${ir.entry}()); }
420+
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}()); }
415421
println!("{:.4}", t.elapsed().as_secs_f64() * 1000.0 / iters as f64);
416422
return;
417423
}
418424
let toks = lex(&src);
419425
let n = toks.len();
420-
let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new() };
426+
let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new(), src: &src };
421427
match p.parse_${ir.entry}() {
422428
Some(root) if p.pos == n => { let mut out = String::new(); write_json(&root, &mut out); print!("{}", out); }
423429
_ => { eprintln!("parse error (pos {}/{})", p.pos, n); std::process::exit(1); }

src/target-ts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
197197
}`;
198198
// Access-tail leds (member/call/index) are disabled once a postfix has closed the operand;
199199
// a precedence-gated led (ternary/in/instanceof) binds only when its lbp > minBp.
200-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if (${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}(_mySup === null || !_mySup.has(${J(b.first)})) && t.text === ${J(b.first)}) {
200+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null, sameLine: boolean, nll: string[] | null) => ` if (${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}${sameLine ? '!t.nl && ' : ''}${nll ? `!${J(nll)}.includes(headLeafText(left)) && ` : ''}(_mySup === null || !_mySup.has(${J(b.first)})) && t.text === ${J(b.first)}) {
201201
const ledSave = pos; const kids: Cst[] = [left];
202202
if (${b.steps.map(stepCond).join(' && ')}) { left = node(${J(r.cstName)}, kids); continue; }
203203
pos = ledSave; break;
@@ -223,7 +223,7 @@ function ${r.name}_bp(minBp: number): Node | null {
223223
for (;;) {
224224
const t = peek();
225225
if (t === null) break;
226-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
226+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i], r.ledSameLine[i], r.ledNotLeftLeaf[i])).join('\n')}
227227
${r.postfixToks.map(postfixArm).join('\n')}
228228
const post = ${r.name}_POST[t.text];
229229
if (!tailClosed && post !== undefined && post > minBp) { pos++; const opLeaf: Leaf = { tokenType: '$operator', offset: t.off, end: t.end }; left = { rule: ${J(r.cstName)}, children: [left, opLeaf], offset: left.offset, end: t.end }; tailClosed = true; continue; }
@@ -302,7 +302,13 @@ let toks: Tok[] = [];
302302
let pos = 0;
303303
let _capped = false;
304304
let _suppressNext: Set<string> | null = null;
305+
let _src = '';
305306
function peek(): Tok | null { return pos < toks.length ? toks[pos] : null; }
307+
function headLeafText(node: Cst): string {
308+
let n: Cst = node;
309+
while ('children' in n && n.children.length > 0) n = n.children[0];
310+
return _src.slice(n.offset, n.end);
311+
}
306312
function branch(rule: string, kids: Cst[], save: number): Node {
307313
const offset = kids.length > 0 ? kids[0].offset : (save < toks.length ? toks[save].off : 0);
308314
const end = kids.length > 0 ? kids[kids.length - 1].end : offset;
@@ -350,6 +356,7 @@ function altLit(opts: [string, string][], kids: Cst[]): boolean {
350356
${matchTemplate}${ruleFns}
351357
352358
const src = readFileSync(0, 'utf8');
359+
_src = src;
353360
toks = lex(src);
354361
pos = 0;
355362
const root = parse${ir.entry}();

test/portable-targets.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,20 @@ const CASES: Case[] = [
195195
],
196196
reject: ['function (', 'a +;', 'if x {}', '{ a: }', 'for (;;', 'a ? b ;'],
197197
},
198+
{
199+
// The real typescript.ts grammar — the second, most complex full language proving the
200+
// agnostic emitter (types, generics, interfaces, enums, assertions, variance). ASCII.
201+
grammar: 'typescript', path: '../typescript.ts',
202+
accept: [
203+
'const a: number = 1;', 'let s: string;', 'type Alias = { a: number; b?: string };',
204+
'type U = "a" | "b" | "c";', 'function gen2<T, U extends T>(x: T, y: U): T { return x; }',
205+
'interface I<T> extends A<T> { m(x: T): T; }', 'const c = x as const;',
206+
'function isStr(x: unknown): x is string { return true; }', 'enum E { A, B, C }',
207+
'const n = maybe!;', 'let arr: number[];', 'type Fn = (x: number) => string;',
208+
'class C<in out T> { value!: T; }',
209+
],
210+
reject: ['interface {}', 'const x: = 1;', 'enum {}', 'a + ;'],
211+
},
198212
];
199213

200214
const sortKeys = (o: unknown): unknown =>

0 commit comments

Comments
 (0)