Skip to content

Commit c494c99

Browse files
Integerize portable parser matching with kid/lid token ids.
Build LexIdPlan from IR and thread u16/number ids through Tok, match_lit/match_tok, Pratt tables, FIRST dispatch, and suppress sets so hot-path compares avoid string memcmp. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c61ddfc commit c494c99

4 files changed

Lines changed: 481 additions & 294 deletions

File tree

src/emit-portable.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,105 @@ function buildPratt(
522522
return { kind: 'pratt', name, cstName, nudToks, nudBrackets, nudSeqs, nudSeqFirst: [], nudSeqPredictive: false, nudCapped, nudCappedFirst: [], prefix, binary, leds, ledAccessTail, ledLbp, ledSameLine, ledNotLeftLeaf, postfixToks, postfix };
523523
}
524524

525+
// ── Lex integer ids (kid = kind index, lid = text/literal index) ──
526+
//
527+
// Parser matching hot paths compare u16/uint16/number instead of strings. kid indexes
528+
// `kids` (token kind names); lid indexes `lids` (literal texts). lid is a pure function of
529+
// text and is independent of kind — matching the existing `match_lit` (text-only) contract.
530+
531+
export type LexIdPlan = {
532+
kids: string[]; // kids[0]='' reserved for punct (kind=""); then tokens in decl order + engine-emitted (NEWLINE, $template*)
533+
lids: string[]; // lids[0]='' = "not any literal"; then puncts (longest-first), then remaining texts lexicographically
534+
};
535+
536+
/** Collect every literal text that appears in the Step IR (match_lit / altlit / sep / suppress / FIRST / Pratt). */
537+
function collectStepLitTexts(steps: Step[], out: Set<string>): void {
538+
for (const s of steps) {
539+
switch (s.t) {
540+
case 'lit': out.add(s.value); break;
541+
case 'altlit': for (const o of s.opts) out.add(o.value); break;
542+
case 'sep': out.add(s.delim); collectStepLitTexts([s.elem], out); break;
543+
case 'suppress': for (const c of s.connectors) out.add(c); collectStepLitTexts(s.steps, out); break;
544+
case 'star': collectStepLitTexts([s.step], out); break;
545+
case 'opt': case 'not': case 'seq': collectStepLitTexts(s.steps, out); break;
546+
case 'alt': for (const b of s.branches) collectStepLitTexts(b, out); break;
547+
default: break;
548+
}
549+
}
550+
}
551+
552+
/**
553+
* Build stable kid/lid tables for a ParserIR. Semantics:
554+
* - kid(kind) = kids.indexOf(kind); punct kind "" → 0.
555+
* - lid(text) = lids.indexOf(text), or 0 if absent. Independent of kind.
556+
*/
557+
export function buildLexIdPlan(ir: ParserIR): LexIdPlan {
558+
// kids: '' + lexer token names (declaration order) + engine-emitted kinds not already present
559+
const kids: string[] = [''];
560+
const seenKid = new Set<string>(['']);
561+
const addKid = (k: string) => { if (!seenKid.has(k)) { seenKid.add(k); kids.push(k); } };
562+
for (const t of ir.tokens) addKid(t.name);
563+
if (ir.newlineCfg) addKid(ir.newlineCfg.token);
564+
if (ir.tpl) {
565+
addKid('$templateHead');
566+
addKid('$templateMiddle');
567+
addKid('$templateTail');
568+
}
569+
570+
// lids universe: puncts + keyword/other lit texts from IR + regexCtx text sets + tpl delimiters
571+
const litTexts = new Set<string>();
572+
for (const p of ir.puncts) litTexts.add(p);
573+
for (const r of ir.rules) {
574+
if (r.kind === 'rd') {
575+
for (const alt of r.alts) collectStepLitTexts(alt, litTexts);
576+
} else {
577+
for (const b of r.nudBrackets) { litTexts.add(b.first); collectStepLitTexts(b.steps, litTexts); }
578+
for (const seq of r.nudSeqs) collectStepLitTexts(seq, litTexts);
579+
for (const c of r.nudCapped) collectStepLitTexts(c.steps, litTexts);
580+
for (const p of r.prefix) litTexts.add(p.op);
581+
for (const b of r.binary) litTexts.add(b.op);
582+
for (const b of r.leds) { litTexts.add(b.first); collectStepLitTexts(b.steps, litTexts); }
583+
for (const p of r.postfix) litTexts.add(p.op);
584+
for (const nll of r.ledNotLeftLeaf) if (nll) for (const x of nll) litTexts.add(x);
585+
}
586+
}
587+
if (ir.regexCtx) {
588+
for (const x of ir.regexCtx.divisionTexts) litTexts.add(x);
589+
for (const x of ir.regexCtx.regexTexts) litTexts.add(x);
590+
for (const x of ir.regexCtx.parenHeadKw) litTexts.add(x);
591+
for (const x of ir.regexCtx.memberAccess) litTexts.add(x);
592+
for (const x of ir.regexCtx.postfixAfterValue) litTexts.add(x);
593+
}
594+
if (ir.tpl) {
595+
litTexts.add(ir.tpl.open);
596+
litTexts.add(ir.tpl.interpOpen);
597+
litTexts.add(ir.tpl.interpClose);
598+
litTexts.add(ir.tpl.braceOpen);
599+
}
600+
if (ir.newlineCfg) {
601+
for (const x of ir.newlineCfg.flowOpen) litTexts.add(x);
602+
for (const x of ir.newlineCfg.flowClose) litTexts.add(x);
603+
}
604+
605+
// Stable order: puncts longest-first (ir.puncts), then remaining texts lexicographically
606+
const punctSet = new Set(ir.puncts);
607+
const rest = [...litTexts].filter((t) => !punctSet.has(t)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
608+
const lids: string[] = ['', ...ir.puncts, ...rest];
609+
return { kids, lids };
610+
}
611+
612+
/** Resolve lid for a known text; 0 if not in the plan (matches runtime lid_of). */
613+
export function lidOf(plan: LexIdPlan, text: string): number {
614+
const i = plan.lids.indexOf(text);
615+
return i >= 0 ? i : 0;
616+
}
617+
618+
/** Resolve kid for a known kind; 0 if not in the plan. */
619+
export function kidOf(plan: LexIdPlan, kind: string): number {
620+
const i = plan.kids.indexOf(kind);
621+
return i >= 0 ? i : 0;
622+
}
623+
525624
// ── Lexer first-byte dispatch (IR analysis; targets render buckets) ──
526625

527626
export type LexFirstBytes = { bytes: number[]; nonAscii: boolean };

0 commit comments

Comments
 (0)