Skip to content

Commit 22c1cf4

Browse files
committed
arrow/cond: ledMixfix re-bind
1 parent a4ef43d commit 22c1cf4

1 file changed

Lines changed: 111 additions & 5 deletions

File tree

src/gen-parser.ts

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,33 @@ export function createParser(grammar: CstGrammar) {
186186
for (const led of leds) ledFirst.set(led, firstTokenOf({ type: 'seq', items: led.items } as RuleExpr));
187187
}
188188

189+
// ── Mixfix operand re-bound info ──
190+
// A LED of the shape `<lit L1> $self <lit L2> …` (e.g. a ternary `? $ : $`) has
191+
// an *inner* operand (`$self`, the bit between L1 and L2) that the greedy
192+
// non-backtracking engine may over-consume — swallowing the L2 the operator
193+
// itself needs (the classic conditional-`?:` vs arrow-return-type-`:` clash:
194+
// `b ? (c) : d => e`, where `(c): d => e` parses as one arrow, leaving no `:`).
195+
// When the normal match of such a LED fails, the LED loop retries the inner
196+
// operand with a position cap so it stops before an L2 at the operator's own
197+
// bracket-nesting depth, freeing that L2 for the operator. Language-agnostic:
198+
// keyed purely on the structural shape, no knowledge of `?`/`:`/arrows.
199+
function selfRefName(e: RuleExpr | undefined, ruleName: string): boolean {
200+
return !!e && e.type === 'ref' && e.name === ruleName;
201+
}
202+
type MixfixInfo = { openLit: string; sepLit: string };
203+
const ledMixfix = new Map<object, MixfixInfo>();
204+
for (const [ruleName, { leds }] of prattClassified.entries()) {
205+
for (const led of leds) {
206+
const it = led.items;
207+
if (it.length >= 4
208+
&& it[0]?.type === 'literal'
209+
&& selfRefName(it[1], ruleName)
210+
&& it[2]?.type === 'literal') {
211+
ledMixfix.set(led, { openLit: it[0].value, sepLit: it[2].value });
212+
}
213+
}
214+
}
215+
189216
// ── FIRST sets ──
190217
// The set of tokens each rule can begin with (null = "anything" — left-recursive
191218
// / prefix-operator rules, which can't be characterized). Used to skip parsing a
@@ -281,8 +308,17 @@ export function createParser(grammar: CstGrammar) {
281308
// token splice (matchLiteral), which shifts later positions.
282309
const memo = new Map<string, Map<number, { node: CstNode | null; end: number }>>();
283310

311+
// Bounded-parse cap (token index). When >= 0, no token at index >= parseLimit
312+
// may be consumed — i.e. a sub-parse is forced to stop before that token. Used
313+
// for the mixfix-operand re-parse (see matchMixfixLed): re-running an over-greedy
314+
// operand so it leaves a required separator for the enclosing mixfix operator.
315+
// `null`-equivalent is -1 (no cap). A capped parse is position+cap dependent,
316+
// so it bypasses the packrat memo (which is keyed by start position only).
317+
let parseLimit = -1;
318+
284319
function peek(): Token | null {
285320
if (pos > maxPos) maxPos = pos;
321+
if (parseLimit >= 0 && pos >= parseLimit) return null;
286322
return tokens[pos] ?? null;
287323
}
288324

@@ -390,10 +426,15 @@ export function createParser(grammar: CstGrammar) {
390426
if (!isPratt && !isLeftRec) return parseNonRec(rule);
391427

392428
// Memoizable (pratt / left-recursive): look up by start position.
429+
// A capped parse (parseLimit active) is position+cap dependent, so it must
430+
// not read or write the position-keyed packrat memo.
431+
const capped = parseLimit >= 0;
393432
const start = pos;
394433
let m = memo.get(name);
395-
const hit = m && m.get(start);
396-
if (hit !== undefined) { pos = hit.end; return hit.node; }
434+
if (!capped) {
435+
const hit = m && m.get(start);
436+
if (hit !== undefined) { pos = hit.end; return hit.node; }
437+
}
397438

398439
const prevContext = currentPrattContext;
399440
currentPrattContext = name;
@@ -403,8 +444,10 @@ export function createParser(grammar: CstGrammar) {
403444
} finally {
404445
currentPrattContext = prevContext;
405446
}
406-
if (!m) { m = new Map(); memo.set(name, m); }
407-
m.set(start, { node: result, end: pos });
447+
if (!capped) {
448+
if (!m) { m = new Map(); memo.set(name, m); }
449+
m.set(start, { node: result, end: pos });
450+
}
408451
return result;
409452
}
410453

@@ -541,7 +584,13 @@ export function createParser(grammar: CstGrammar) {
541584
if (!canStart(ledFirst.get(led), tok)) continue; // first-token dispatch for LED continuations
542585

543586
pos = ledSaved;
544-
const children = matchSeq(led.items);
587+
let children = matchSeq(led.items);
588+
// Mixfix operand re-bound: if a `<L1> $ <L2> …` LED failed, the inner
589+
// operand may have over-consumed the L2 it needs — retry it capped.
590+
if (children === null && ledMixfix.has(led)) {
591+
pos = ledSaved;
592+
children = matchMixfixLed(led, rule.name, ledMixfix.get(led)!);
593+
}
545594
if (children !== null) {
546595
lhs = {
547596
kind: 'node',
@@ -647,6 +696,63 @@ export function createParser(grammar: CstGrammar) {
647696
return children;
648697
}
649698

699+
// Mixfix operand re-bound (see ledMixfix). The LED shape is
700+
// `<openLit> $self <sepLit> …rest`; the normal match already failed. Re-parse
701+
// the inner `$self` operand with a position cap so it stops before a `sepLit`
702+
// token at the *same* bracket depth as `openLit`, freeing that token for the
703+
// operator's own separator, then match `sepLit` and the rest uncapped.
704+
// `pos` is at `openLit` on entry. Returns the LED children or null (restored).
705+
function matchMixfixLed(led: { items: RuleExpr[] }, ruleName: string, info: { openLit: string; sepLit: string }): CstChild[] | null {
706+
const saved = pos;
707+
const openLeaf = matchLiteral(info.openLit);
708+
if (!openLeaf) { pos = saved; return null; }
709+
const afterOpen = pos;
710+
711+
// Greedy parse of the operand, to (a) confirm the separator is missing right
712+
// after it (otherwise the failure is elsewhere → don't apply) and (b) bound
713+
// the scan window.
714+
const operand = parseRule(ruleName);
715+
if (!operand) { pos = saved; return null; }
716+
const greedyEnd = pos;
717+
// If the separator DOES match here, the LED's failure was later in `rest`,
718+
// not operand over-consumption — re-bounding wouldn't help.
719+
if (matchLiteral(info.sepLit)) { pos = saved; return null; }
720+
721+
// Candidate separator positions: `sepLit` tokens at bracket depth 0 within
722+
// (afterOpen, greedyEnd). A nested same-shape operator (e.g. a nested
723+
// ternary) contributes its own depth-0 `sepLit`, so we try candidates in
724+
// order and accept the first where the capped operand lands exactly on it.
725+
let depth = 0;
726+
const candidates: number[] = [];
727+
for (let i = afterOpen; i < greedyEnd; i++) {
728+
const t = tokens[i];
729+
if (t.type !== '') continue; // only punctuation carries brackets/sep
730+
if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
731+
else if (t.text === ')' || t.text === ']' || t.text === '}') depth--;
732+
else if (depth === 0 && t.text === info.sepLit) candidates.push(i);
733+
}
734+
735+
for (const sepIdx of candidates) {
736+
// Re-parse the operand capped so it cannot consume the token at sepIdx;
737+
// accept only if the operand consumes everything up to exactly there.
738+
pos = afterOpen;
739+
const prevLimit = parseLimit;
740+
parseLimit = sepIdx;
741+
const reOperand = parseRule(ruleName);
742+
parseLimit = prevLimit;
743+
if (!reOperand || pos !== sepIdx) continue;
744+
745+
const sepLeaf = matchLiteral(info.sepLit);
746+
if (!sepLeaf) continue;
747+
const rest = matchSeq(led.items.slice(3));
748+
if (rest === null) continue;
749+
return [openLeaf, reOperand, sepLeaf, ...rest];
750+
}
751+
752+
pos = saved;
753+
return null;
754+
}
755+
650756
function matchQuantifier(body: RuleExpr, kind: '*' | '+' | '?'): CstChild[] | null {
651757
if (kind === '?') {
652758
const result = matchExpr(body);

0 commit comments

Comments
 (0)