Skip to content

Commit c920b99

Browse files
Slim portable arena to 32-byte nodes with inlined leaf kids.
Rule nodes keep only rule_id + u32 spans; leaves encode as negative kids/scratch entries (tok_idx, tt_id) backed by TT_NAMES/RULE_NAMES, cutting arena write traffic on leaf-heavy trees without changing CST JSON. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c494c99 commit c920b99

3 files changed

Lines changed: 588 additions & 285 deletions

File tree

src/emit-portable.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,93 @@ export function kidOf(plan: LexIdPlan, kind: string): number {
621621
return i >= 0 ? i : 0;
622622
}
623623

624+
// ── Arena id tables (rule_id / tt_id for slim Node + inlined leaves) ──
625+
//
626+
// Arena stores only rule nodes; leaf kids are negative i32 encodings that pack
627+
// (tok_idx, tt_id). TT_NAMES is indexed by tt_id; RULE_NAMES by rule_id.
628+
// TT_NAMES starts with LexIdPlan.kids so token-kind leaves can use Tok.kid as tt_id.
629+
630+
export type ArenaIdPlan = {
631+
ttNames: string[]; // TT_NAMES — leaf tokenType strings
632+
ruleNames: string[]; // RULE_NAMES — CST rule labels
633+
};
634+
635+
/** Sentinel tt_id for $punct (trivia — never stored as a leaf). Not an index into ttNames. */
636+
export const TT_SKIP_PUNCT = 255;
637+
638+
/** Collect every ttype text that push_leaf / match_lit|match_tok may emit (excluding $punct). */
639+
function collectStepTtypes(steps: Step[], out: Set<string>): void {
640+
for (const s of steps) {
641+
switch (s.t) {
642+
case 'lit': if (s.ttype !== '$punct') out.add(s.ttype); break;
643+
case 'tok': out.add(s.name); break;
644+
case 'altlit': for (const o of s.opts) if (o.ttype !== '$punct') out.add(o.ttype); break;
645+
case 'sep': collectStepTtypes([s.elem], out); break;
646+
case 'suppress': collectStepTtypes(s.steps, out); break;
647+
case 'star': collectStepTtypes([s.step], out); break;
648+
case 'opt': case 'not': case 'seq': collectStepTtypes(s.steps, out); break;
649+
case 'alt': for (const b of s.branches) collectStepTtypes(b, out); break;
650+
default: break;
651+
}
652+
}
653+
}
654+
655+
/**
656+
* Build TT_NAMES + RULE_NAMES for arena slim encoding.
657+
* TT_NAMES = kids 全体 ∪ {$keyword,$operator,$templateHead,$templateMiddle,$templateTail}
658+
* ∪ other push_leaf ttypes collected from the IR (exhaustive).
659+
* RULE_NAMES = every rule cstName ∪ {$template} when templates are enabled.
660+
*/
661+
export function buildArenaIdPlan(ir: ParserIR, lexIds: LexIdPlan): ArenaIdPlan {
662+
const ttNames = [...lexIds.kids];
663+
const seenTt = new Set<string>(ttNames);
664+
const addTt = (s: string) => { if (!seenTt.has(s)) { seenTt.add(s); ttNames.push(s); } };
665+
for (const s of ['$keyword', '$operator', '$templateHead', '$templateMiddle', '$templateTail']) addTt(s);
666+
667+
const fromSteps = new Set<string>();
668+
for (const r of ir.rules) {
669+
if (r.kind === 'rd') {
670+
for (const alt of r.alts) collectStepTtypes(alt, fromSteps);
671+
} else {
672+
for (const t of r.nudToks) fromSteps.add(t);
673+
for (const b of r.nudBrackets) collectStepTtypes(b.steps, fromSteps);
674+
for (const seq of r.nudSeqs) collectStepTtypes(seq, fromSteps);
675+
for (const c of r.nudCapped) collectStepTtypes(c.steps, fromSteps);
676+
for (const b of r.leds) collectStepTtypes(b.steps, fromSteps);
677+
for (const t of r.postfixToks) fromSteps.add(t);
678+
}
679+
}
680+
if (ir.newlineCfg) fromSteps.add(ir.newlineCfg.token);
681+
for (const s of fromSteps) addTt(s);
682+
683+
if (ttNames.length > 64) {
684+
throw new Error(`TT_NAMES length ${ttNames.length} exceeds tt_id limit 63`);
685+
}
686+
687+
const ruleNames: string[] = [];
688+
const seenR = new Set<string>();
689+
const addR = (s: string) => { if (!seenR.has(s)) { seenR.add(s); ruleNames.push(s); } };
690+
for (const r of ir.rules) addR(r.cstName);
691+
if (ir.tpl) addR('$template');
692+
693+
return { ttNames, ruleNames };
694+
}
695+
696+
/** Resolve tt_id for a leaf tokenType; throws if missing (codegen bug). $punct → TT_SKIP_PUNCT. */
697+
export function ttIdOf(plan: ArenaIdPlan, ttype: string): number {
698+
if (ttype === '$punct') return TT_SKIP_PUNCT;
699+
const i = plan.ttNames.indexOf(ttype);
700+
if (i < 0) throw new Error(`ttIdOf: unknown ttype ${JSON.stringify(ttype)}`);
701+
return i;
702+
}
703+
704+
/** Resolve rule_id for a CST rule label; throws if missing. */
705+
export function ruleIdOf(plan: ArenaIdPlan, rule: string): number {
706+
const i = plan.ruleNames.indexOf(rule);
707+
if (i < 0) throw new Error(`ruleIdOf: unknown rule ${JSON.stringify(rule)}`);
708+
return i;
709+
}
710+
624711
// ── Lexer first-byte dispatch (IR analysis; targets render buckets) ──
625712

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

0 commit comments

Comments
 (0)