Skip to content

Commit 544e277

Browse files
committed
emit-portable: precedence-gated mixfix LEDs (ternary + chain-rhs in/instanceof)
The portable parser's mixfix leds bound maximally tight — fine for access tails (`.`/`(`/`[`) but wrong for a precedence-carrying led like the ternary `? :` (`a == b ? c : d` must group as `(a == b) ? c : d`). The led loop now gates such a led by its lbp (from the grammar's ledPrec): bind only when lbp > minBp. And a chain-rhs led (`in`/`instanceof`) parses its trailing self-operand at the level's bp via a new `ruleBp` step, so `a in b in c` left-chains as `(a in b) in c`. Both derive from analyzeGrammar's ledPrecByConnector — single-sourced with the interpreter. examples/ledjs.ts verifies it across ts/go/rust — 11/11 accept (ternary below the operators, right-associative `a ? b : c ? d : e`, chain-rhs `in`), 4/4 reject, byte-identical to createParser. Full suite 42/42. This is the precedence foundation the no-`in` (suppress) context builds on next.
1 parent 395ba51 commit 544e277

6 files changed

Lines changed: 80 additions & 10 deletions

File tree

examples/ledjs.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Exercises precedence-gated mixfix LEDs: the ternary `? :` (a led that binds LOOSER than the
2+
// operators, so `a == b ? c : d` groups as `(a == b) ? c : d`) and `in`/`instanceof` (chain-rhs
3+
// leds at the relational level — `a in b in c` left-chains as `(a in b) in c`). Both need the
4+
// led-precedence gate the portable parser previously lacked (its mixfix leds bound maximally tight).
5+
import {
6+
token, rule, defineGrammar, left, right, op,
7+
seq, oneOf, range, star, many,
8+
} from '../src/api.ts';
9+
10+
const idStart = oneOf(range('a', 'z'), range('A', 'Z'), '_', '$');
11+
const idCont = oneOf(range('a', 'z'), range('A', 'Z'), range('0', '9'), '_', '$');
12+
const Ident = token(seq(idStart, star(idCont)), { identifier: true, scope: 'variable' });
13+
const Number_ = token(seq(range('0', '9'), star(range('0', '9'))), { scope: 'constant.numeric' });
14+
15+
const jsPrec = [
16+
right('='),
17+
left('||'),
18+
left('==', '!='),
19+
left('<', '>'),
20+
left('+', '-'),
21+
left('*', '/'),
22+
];
23+
24+
const Expr = rule(($) => [
25+
Number_, Ident,
26+
['(', $, ')'],
27+
[$, op, $],
28+
[$, '?', $, ':', $], // ternary (binds below `||`)
29+
[$, 'in', $], // relational chain-rhs
30+
[$, 'instanceof', $],
31+
]);
32+
const Stmt = rule(($) => [[Expr, ';']]);
33+
const Program = rule(($) => [many(Stmt)]);
34+
35+
export default defineGrammar({
36+
name: 'ledjs',
37+
scopeName: 'source.ledjs',
38+
tokens: { Ident, Number: Number_ },
39+
prec: jsPrec,
40+
ledPrec: [
41+
{ connector: '?', below: '||' },
42+
{ connector: 'in', sameAs: '<', chainRhs: true },
43+
{ connector: 'instanceof', sameAs: '<', chainRhs: true },
44+
],
45+
rules: { Expr, Stmt, Program },
46+
});

src/emit-portable.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type Step =
4646
| { t: 'lit'; value: string; ttype: '$keyword' | '$punct' } // match a literal by text
4747
| { t: 'tok'; name: string } // match a token kind
4848
| { t: 'rule'; name: string } // call a rule, append its node
49+
| { t: 'ruleBp'; name: string; bp: number } // call a Pratt rule at a given binding power (chain-rhs led trailing operand)
4950
| { t: 'star'; step: Step } // repeat inner 0+
5051
| { t: 'opt'; steps: Step[] } // optional sub-sequence
5152
| { t: 'sep'; elem: Step; delim: string } // elem (delim elem)*
@@ -69,6 +70,7 @@ export type PrattRule = {
6970
binary: Array<{ op: string; lbp: number; rbp: number }>; // LED: infix op, bind iff lbp > minBp, rhs at rbp
7071
leds: Bracket[]; // LED: mixfix continuation (call/member/index), tried before operators
7172
ledAccessTail: boolean[]; // parallel to leds: a "closed punct-connector" tail (member/call/index) — disabled once a postfix binds
73+
ledLbp: Array<number | null>; // parallel to leds: precedence gate (ternary/in/instanceof) — bind only when lbp > minBp; null = bind maximally tight
7274
postfixToks: string[]; // LED: a postfix token `$ X` (e.g. a tagged template), tried like a mixfix led (also an access tail)
7375
postfix: Array<{ op: string; lbp: number }>; // LED: a postfix operator `$ ++` — binds iff lbp > minBp + !tailClosed, no rhs, closes the tail
7476
};
@@ -268,6 +270,7 @@ function buildPratt(
268270
let sawPrefix = false, sawBinary = false, sawPostfix = false;
269271
const leds: Bracket[] = [];
270272
const ledAccessTail: boolean[] = [];
273+
const ledLbp: Array<number | null> = [];
271274
const postfixToks: string[] = [];
272275
for (const alt of alts) {
273276
const items = alt.type === 'seq' ? alt.items : [alt];
@@ -303,12 +306,17 @@ function buildPratt(
303306
if (rest[0].type === 'op') { sawBinary = true; continue; }
304307
if (rest[0].type === 'postfix') { sawPostfix = true; continue; } // postfix operator (`x++`)
305308
if (rest[0].type === 'literal') {
309+
const conn = rest[0].value;
310+
const prec = a.ledPrecByConnector.get(conn); // { lbp, rhsBp } for ternary/in/instanceof
306311
const steps = rest.map((it) => stepOfPratt(it));
307312
const last = steps[steps.length - 1];
308313
const lastIsOperand = last !== undefined && last.t === 'rule' && last.name === name; // open binary/ternary operand
309-
const wordConnector = /^[A-Za-z]/.test(rest[0].value); // `in`/`instanceof`/`as` — not a tail
310-
leds.push({ first: rest[0].value, steps });
314+
// chain-rhs (`in`/`instanceof`): the trailing self-operand parses at the level's bp (left-chain).
315+
if (prec && prec.rhsBp !== null && lastIsOperand) steps[steps.length - 1] = { t: 'ruleBp', name, bp: prec.rhsBp };
316+
const wordConnector = /^[A-Za-z]/.test(conn); // `in`/`instanceof`/`as` — not a tail
317+
leds.push({ first: conn, steps });
311318
ledAccessTail.push(!lastIsOperand && !wordConnector);
319+
ledLbp.push(prec ? prec.lbp : null);
312320
continue;
313321
}
314322
if (rest.length === 1 && rest[0].type === 'ref' && a.tokenNames.has(rest[0].name)) { postfixToks.push(rest[0].name); continue; } // postfix token (tagged template)
@@ -337,5 +345,5 @@ function buildPratt(
337345
const postfix = sawPostfix
338346
? [...a.opTable.entries()].filter(([, info]) => info.position === 'postfix').map(([op, info]) => ({ op, lbp: info.lbp }))
339347
: [];
340-
return { kind: 'pratt', name, nudToks, nudBrackets, nudSeqs, nudCapped, prefix, binary, leds, ledAccessTail, postfixToks, postfix };
348+
return { kind: 'pratt', name, nudToks, nudBrackets, nudSeqs, nudCapped, prefix, binary, leds, ledAccessTail, ledLbp, postfixToks, postfix };
341349
}

src/target-go.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ function stepCond(s: Step): string {
169169
case 'lit': return `matchLit(${J(s.value)}, ${J(s.ttype)})`;
170170
case 'tok': return `matchTok(${J(s.name)})`;
171171
case 'rule': return `callRule(parse${s.name})`;
172+
case 'ruleBp': return `callRule(func() int32 { return ${s.name}bp(${s.bp}) })`;
172173
case 'star': return `star(func() bool { return ${stepCond(s.step)} })`;
173174
case 'opt': return `opt(func() bool { return ${s.steps.map(stepCond).join(' && ')} })`;
174175
case 'sep': return `sepBy(func() bool { return ${stepCond(s.elem)} }, ${J(s.delim)})`;
@@ -208,7 +209,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
208209
\t\tif ${b.steps.map(stepCond).join(' && ')} { return finish(${J(r.name)}, sb, t.Off) }
209210
\t\tpos = save; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]; return -1
210211
\t}`;
211-
const ledArm = (b: Bracket, accessTail: boolean) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}t.Text == ${J(b.first)} {
212+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}t.Text == ${J(b.first)} {
212213
\t\t\tledSave := pos; sb := len(scratch); nb := len(nodes); kb := len(kids)
213214
\t\t\tscratch = append(scratch, left)
214215
\t\t\tif ${b.steps.map(stepCond).join(' && ')} { left = finish(${J(r.name)}, sb, nodes[left].Offset); continue }
@@ -239,7 +240,7 @@ func ${r.name}bp(minBp int) int32 {
239240
\tfor {
240241
\t\tt := peek()
241242
\t\tif t == nil { break }
242-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i])).join('\n')}
243+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
243244
${r.postfixToks.map(postfixArm).join('\n')}
244245
\t\tif post, ok := ${r.name}POST[t.Text]; ok && !tailClosed && post > minBp {
245246
\t\t\tsb := len(scratch); scratch = append(scratch, left, mkLeaf("$operator", t.Off, t.End)); pos++; tailClosed = true

src/target-rust.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ function stepCond(s: Step): string {
177177
case 'lit': return `self.match_lit(${J(s.value)}, ${J(s.ttype)}, &mut kids)`;
178178
case 'tok': return `self.match_tok(${J(s.name)}, &mut kids)`;
179179
case 'rule': return `self.call_rule(Parser::parse_${s.name}, &mut kids)`;
180+
case 'ruleBp': return `self.call_rule(|p| p.${s.name}_bp(${s.bp}), &mut kids)`;
180181
case 'star': return `self.star(|p, k| ${stepCondP(s.step)}, &mut kids)`;
181182
case 'opt': return `self.opt(|p, k| ${s.steps.map(stepCondP).join(' && ')}, &mut kids)`;
182183
case 'sep': return `self.sep_by(|p, k| ${stepCondP(s.elem)}, ${J(s.delim)}, &mut kids)`;
@@ -202,6 +203,7 @@ function stepCondP(s: Step): string {
202203
case 'lit': return `p.match_lit(${J(s.value)}, ${J(s.ttype)}, k)`;
203204
case 'tok': return `p.match_tok(${J(s.name)}, k)`;
204205
case 'rule': return `p.call_rule(Parser::parse_${s.name}, k)`;
206+
case 'ruleBp': return `p.call_rule(|p| p.${s.name}_bp(${s.bp}), k)`;
205207
case 'star': return `p.star(|p, k| ${stepCondP(s.step)}, k)`;
206208
case 'opt': return `p.opt(|p, k| ${s.steps.map(stepCondP).join(' && ')}, k)`;
207209
case 'sep': return `p.sep_by(|p, k| ${stepCondP(s.elem)}, ${J(s.delim)}, k)`;
@@ -237,7 +239,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
237239
if ${b.steps.map(stepCond).join(' && ')} { return Some(node(${J(r.name)}, kids)); }
238240
self.pos = save; return None;
239241
}`;
240-
const ledArm = (b: Bracket, accessTail: boolean) => ` if ${accessTail ? '!tail_closed && ' : ''}t.text == ${J(b.first)} {
242+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if ${accessTail ? '!tail_closed && ' : ''}${lbp !== null ? `${lbp} > min_bp && ` : ''}t.text == ${J(b.first)} {
241243
let led_save = self.pos; let mut kids: Vec<Cst> = Vec::new();
242244
if ${b.steps.map(stepCond).join(' && ')} {
243245
let mut full = vec![left]; full.append(&mut kids);
@@ -262,7 +264,7 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
262264
let mut tail_closed = false;
263265
loop {
264266
let t = match self.peek() { Some(t) => t, None => break };
265-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i])).join('\n')}
267+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
266268
${r.postfixToks.map(postfixArm).join('\n')}
267269
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.name)}, vec![left, op_leaf]); tail_closed = true; continue; } }
268270
let (lbp, rbp) = match Parser::${r.name}_bin(t.text) { Some(x) => x, None => break };

src/target-ts.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function stepCond(s: Step): string {
160160
case 'lit': return `matchLit(${J(s.value)}, ${J(s.ttype)}, kids)`;
161161
case 'tok': return `matchTok(${J(s.name)}, kids)`;
162162
case 'rule': return `callRule(parse${s.name}, kids)`;
163+
case 'ruleBp': return `callRule(() => ${s.name}_bp(${s.bp}), kids)`;
163164
case 'star': return `star(() => ${stepCond(s.step)}, kids)`;
164165
case 'opt': return `opt(() => ${s.steps.map(stepCond).join(' && ')}, kids)`;
165166
case 'sep': return `sepBy(() => ${stepCond(s.elem)}, ${J(s.delim)}, kids)`;
@@ -193,8 +194,9 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
193194
if (${b.steps.map(stepCond).join(' && ')}) return node(${J(r.name)}, kids);
194195
pos = save; return null;
195196
}`;
196-
// Access-tail leds (member/call/index) are disabled once a postfix has closed the operand.
197-
const ledArm = (b: Bracket, accessTail: boolean) => ` if (${accessTail ? '!tailClosed && ' : ''}t.text === ${J(b.first)}) {
197+
// Access-tail leds (member/call/index) are disabled once a postfix has closed the operand;
198+
// a precedence-gated led (ternary/in/instanceof) binds only when its lbp > minBp.
199+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if (${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}t.text === ${J(b.first)}) {
198200
const ledSave = pos; const kids: Cst[] = [left];
199201
if (${b.steps.map(stepCond).join(' && ')}) { left = node(${J(r.name)}, kids); continue; }
200202
pos = ledSave; break;
@@ -219,7 +221,7 @@ function ${r.name}_bp(minBp: number): Node | null {
219221
for (;;) {
220222
const t = peek();
221223
if (t === null) break;
222-
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i])).join('\n')}
224+
${r.leds.map((b, i) => ledArm(b, r.ledAccessTail[i], r.ledLbp[i])).join('\n')}
223225
${r.postfixToks.map(postfixArm).join('\n')}
224226
const post = ${r.name}_POST[t.text];
225227
if (!tailClosed && post !== undefined && post > minBp) { pos++; const opLeaf: Leaf = { tokenType: '$operator', offset: t.off, end: t.end }; left = { rule: ${J(r.name)}, children: [left, opLeaf], offset: left.offset, end: t.end }; tailClosed = true; continue; }

test/portable-targets.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ const CASES: Case[] = [
154154
],
155155
reject: ['=> x;', 'x => ;', '1 + () => 2;', '(,) => b;'],
156156
},
157+
{
158+
// Precedence-gated mixfix LEDs: ternary `? :` (binds below the operators) and the
159+
// chain-rhs relational leds `in`/`instanceof` (`a in b in c` left-chains).
160+
grammar: 'ledjs', path: '../examples/ledjs.ts',
161+
accept: [
162+
'a == b ? c : d;', 'a ? b : c ? d : e;', 'a + b ? c : d - e;', 'a in b;',
163+
'a in b in c;', 'x instanceof Y;', 'a < b in c;', '1 + 2 * 3 ? 4 : 5;',
164+
'(a ? b : c) + d;', 'a in b ? c : d;', 'a = b ? c : d;',
165+
],
166+
reject: ['a ? b;', 'a ? : c;', 'in b;', 'a instanceof;'],
167+
},
157168
];
158169

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

0 commit comments

Comments
 (0)