Skip to content

Commit 55a5e79

Browse files
committed
Flatten AST records and locate base fields by offset table
Records embedded shared base structs (declaration, function-like, and so on), each padded to its own alignment, and every base field read went through a switch on the node kind to find the right embedded struct. Laying each record out as the header followed by all of its fields, sorted by alignment, removes the padding between the parts, and a per-kind table of field offsets replaces the switch with a single indexed load. │ nested │ flat │ │ check-sec │ check-sec vs base │ VSCode 7.344 ± 1% 7.391 ± 2% ~ (p=0.542 n=10) │ nested │ flat │ │ mem-B │ mem-B vs base │ VSCode 3.412Gi ± 0% 3.398Gi ± 0% -0.39% (p=0.000 n=10)
1 parent c68646c commit 55a5e79

4 files changed

Lines changed: 1722 additions & 2191 deletions

File tree

‎tools/scripts/tsc/generate-go-ast.ts‎

Lines changed: 104 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,74 @@ function baseGoFields(key: string): GoField[] {
302302
for (const ext of baseGoEmbeds(base)) fields.push(...baseGoFields(ext));
303303
for (const field of base.fields) {
304304
if (field.noGo) continue;
305-
fields.push({ name: field.name, goType: field.goOnly ? field.rawType as string : field.type.formatGoReference(), isNode: isNodeField(field) });
305+
fields.push({ name: field.name, goType: field.goOnly ? field.rawType as string : field.type.formatGoReference(), isNode: isNodeField(field), owner: key });
306306
}
307307
return fields;
308308
}
309309

310+
// A field as laid out in a record.
311+
interface RecordField {
312+
storage: string;
313+
goType: string;
314+
comment: string;
315+
}
316+
317+
function baseRecordFields(key: string): RecordField[] {
318+
const base = api.getBase(key);
319+
if (!base || HAND_WRITTEN_BASES.has(key)) return [];
320+
const fields: RecordField[] = [];
321+
for (const ext of baseGoEmbeds(base)) fields.push(...baseRecordFields(ext));
322+
for (const field of base.fields) {
323+
if (field.noGo) continue;
324+
const valueType = field.goOnly ? field.rawType as string : field.type.formatGoReference();
325+
const goType = recordGoType(isNodeField(field), valueType);
326+
const parts = [];
327+
if (goType !== valueType) parts.push(valueType);
328+
if (field.optional) parts.push("Optional");
329+
fields.push({ storage: goStorageName(field.name), goType, comment: parts.length > 0 ? `// ${parts.join(". ")}` : "" });
330+
}
331+
return fields;
332+
}
333+
334+
// Alignments of the Go types that record fields may have.
335+
const recordFieldAlignments: Record<string, number> = {
336+
"nodeRef": 8,
337+
"textRef": 4,
338+
"sideRef": 4,
339+
"atomic.Uint32": 4,
340+
"TokenFlags": 4,
341+
"Kind": 2,
342+
"ImportPhaseModifierSyntaxKind": 2,
343+
"bool": 1,
344+
};
345+
346+
function recordFieldAlignment(goType: string): number {
347+
if (!Object.hasOwn(recordFieldAlignments, goType)) {
348+
throw new Error(`unknown alignment for record field type ${goType}`);
349+
}
350+
return recordFieldAlignments[goType];
351+
}
352+
353+
// A node's record holds its own and its bases' fields after the header,
354+
// ordered by decreasing alignment so that no padding falls between them.
355+
function nodeRecordFields(node: NodeType): RecordField[] {
356+
const fields: RecordField[] = [];
357+
for (const ext of goEmbeds(node.extendsKeys)) fields.push(...baseRecordFields(ext));
358+
for (const m of nodeOwnFields(node)) {
359+
const goType = recordGoType(isNodeField(m), memberGoType(m));
360+
fields.push({ storage: goStorageName(m.name), goType, comment: buildFieldComment(m, goType !== memberGoType(m) ? memberGoType(m) : undefined) });
361+
}
362+
const seen = new Set<string>();
363+
for (const field of fields) {
364+
if (seen.has(field.storage)) throw new Error(`${node.name} has more than one field named ${field.storage}`);
365+
seen.add(field.storage);
366+
}
367+
return fields
368+
.map((field, index) => ({ field, index }))
369+
.sort((a, b) => recordFieldAlignment(b.field.goType) - recordFieldAlignment(a.field.goType) || a.index - b.index)
370+
.map(({ field }) => field);
371+
}
372+
310373
function nodeGoFields(node: NodeType): GoField[] {
311374
const fields: GoField[] = [];
312375
for (const ext of goEmbeds(node.extendsKeys)) fields.push(...baseGoFields(ext));
@@ -318,33 +381,18 @@ function generateStructDef(w: CodeWriter, node: NodeType) {
318381
const structName = node.name;
319382
w.write(`type ${dataName(node)} struct {`);
320383
w.push();
321-
322-
// Embeddings from extends (each maps to a Go struct via convention)
323-
for (const ext of goEmbeds(node.extendsKeys)) {
324-
w.write(baseDataName(ext));
325-
}
326-
327-
// Fields from members (skip inherited, Kind params, and noGo)
328-
for (const m of nodeOwnFields(node)) {
329-
const fieldName = goStorageName(m.name);
330-
const goType = recordGoType(isNodeField(m), memberGoType(m));
331-
const comment = buildFieldComment(m, goType !== memberGoType(m) ? memberGoType(m) : undefined);
332-
if (comment) {
333-
w.write(`${fieldName} ${goType} ${comment}`);
334-
}
335-
else {
336-
w.write(`${fieldName} ${goType}`);
337-
}
384+
w.write("nodeBaseData");
385+
for (const { storage, goType, comment } of nodeRecordFields(node)) {
386+
w.write(comment ? `${storage} ${goType} ${comment}` : `${storage} ${goType}`);
338387
}
339-
340388
w.pop();
341389
w.write("}");
342390
w.write("");
343391
w.write(`type ${structName} struct{ Node }`);
344392
w.write("");
345393
w.write(`func (node ${structName}) data() *${dataName(node)} { return ${dataOf(node, "node")} }`);
346394
w.write("");
347-
generateFieldAccessors(w, structName, nodeGoFields(node), "node.data()");
395+
generateFieldAccessors(w, structName, nodeGoFields(node), f => `&node.data().${goStorageName(f.name)}`, f => `node.data().${goStorageName(f.name)}`);
348396
}
349397

350398
function buildFieldComment(m: MemberInfo, refType?: string): string {
@@ -401,79 +449,56 @@ const HAND_WRITTEN_BASES = new Set(["NodeBase"]);
401449

402450
function generateBaseStructDefs(w: CodeWriter) {
403451
w.write("// ──────────────────────────────────────────────────────────────────────");
404-
w.write("// Base struct definitions");
452+
w.write("// Base views");
405453
w.write("// ──────────────────────────────────────────────────────────────────────");
406454
w.write("");
407455

408456
for (const base of api.bases()) {
409457
if (HAND_WRITTEN_BASES.has(base.key)) continue;
410458

411459
const structName = base.key;
412-
413-
const goExts = baseGoEmbeds(base);
414-
415-
w.write(`type ${baseDataName(structName)} struct {`);
416-
w.push();
417-
418-
// Embeddings from extends
419-
for (const ext of goExts) {
420-
w.write(baseDataName(ext));
421-
}
422-
423-
// Schema fields
424460
for (const field of base.fields) {
425-
if (field.noGo) continue;
426-
const valueType = field.goOnly ? field.rawType as string : field.type.formatGoReference();
427-
const goType = recordGoType(isNodeField(field), valueType);
428-
const parts = [];
429-
if (goType !== valueType) parts.push(valueType);
430-
if (field.optional) parts.push("Optional");
431-
const comment = parts.length > 0 ? ` // ${parts.join(". ")}` : "";
432-
w.write(`${goStorageName(field.name)} ${goType}${comment}`);
461+
if (!field.noGo) generateBaseFieldOffsets(w, base, field.name, field.goOnly ? field.rawType as string : field.type.formatGoReference(), isNodeField(field));
433462
}
434463

435-
w.pop();
436-
w.write("}");
437-
w.write("");
438-
439464
// Base views see the fields of whichever concrete node they wrap.
440465
w.write(`type ${structName} struct{ Node }`);
441466
w.write("");
442467
const fields = baseGoFields(structName);
443468
if (fields.length > 0) {
444-
generateBaseDataDispatch(w, base);
445-
generateFieldAccessors(w, structName, fields, `node.${baseDataName(structName)}()`);
469+
generateFieldAccessors(w, structName, fields, f => `node.${baseFieldName(f.owner!, f.name)}()`, f => `*node.${baseFieldName(f.owner!, f.name)}()`);
446470
}
447471
}
448472
}
449473

450-
function generateBaseDataDispatch(w: CodeWriter, base: NodeType) {
451-
const cases: { node: NodeType; kinds: string[]; }[] = [];
452-
for (const node of api.nodes()) {
453-
if (!transitiveBaseKeys(node).has(base.key)) continue;
454-
const kinds = node.allKinds().map(kind => kind.formatGoConstant());
455-
if (kinds.length !== 0) cases.push({ node, kinds });
456-
}
457-
const table = `${baseDataName(base.key)}Dispatch`;
474+
// The method locating a base's field in a record.
475+
function baseFieldName(key: string, name: string): string {
476+
return `${goStorageName(key)}${api.capitalize(goStorageName(name))}`;
477+
}
478+
479+
// A base's fields lie at different offsets in each kind's record, so each
480+
// has a table of its offsets by kind, zero for kinds without the base.
481+
function generateBaseFieldOffsets(w: CodeWriter, base: NodeType, name: string, valueType: string, isNode: boolean) {
482+
const method = baseFieldName(base.key, name);
483+
const table = `${method}Offsets`;
484+
const goType = recordGoType(isNode, valueType);
458485
w.write(`var ${table} = [kindFlowReduceLabelData + 1]uint8{`);
459486
w.push();
460-
for (const [index, { kinds }] of cases.entries()) {
461-
for (const kind of kinds) {
462-
w.write(`${kind}: ${index + 1},`);
487+
for (const node of api.nodes()) {
488+
if (!transitiveBaseKeys(node).has(base.key)) continue;
489+
for (const kind of node.allKinds()) {
490+
w.write(`${kind.formatGoConstant()}: uint8(unsafe.Offsetof(${dataName(node)}{}.${goStorageName(name)})),`);
463491
}
464492
}
465493
w.pop();
466494
w.write("}");
467495
w.write("");
468-
w.write(`func (n Node) ${baseDataName(base.key)}() *${baseDataName(base.key)} {`);
496+
w.write(`func (n Node) ${method}() *${goType} {`);
469497
w.push();
470-
w.write(`switch ${table}[n.Kind()] {`);
471-
for (const [index, { node }] of cases.entries()) {
472-
w.write(`case ${index + 1}:`);
473-
w.push();
474-
w.write(`return &${dataOf(node, "n")}.${baseDataName(base.key)}`);
475-
w.pop();
476-
}
498+
w.write(`if off := ${table}[n.Kind()]; off != 0 {`);
499+
w.push();
500+
w.write(`return (*${goType})(unsafe.Add(n.p, off))`);
501+
w.pop();
477502
w.write("}");
478503
w.write("return nil");
479504
w.pop();
@@ -626,16 +651,14 @@ interface GoField {
626651
name: string;
627652
goType: string;
628653
isNode: boolean;
654+
// The base declaring the field, or undefined for a node's own field.
655+
owner?: string;
629656
}
630657

631658
function dataName(node: NodeType): string {
632659
return node.handWritten ? node.name : `${goStorageName(node.name)}Data`;
633660
}
634661

635-
function baseDataName(key: string): string {
636-
return key === "NodeBase" ? "nodeBaseData" : `${goStorageName(key)}Data`;
637-
}
638-
639662
// Private fields get exported getters only where they agree with the shared
640663
// Node method of the same name; others would shadow it with a different type.
641664
function hasPublicGetter(name: string, goType: string): boolean {
@@ -660,23 +683,24 @@ function viewOf(node: NodeType, recv: string): string {
660683
return node.handWritten ? dataOf(node, recv) : `${node.name}{${recv}}`;
661684
}
662685

663-
function generateFieldAccessors(w: CodeWriter, structName: string, fields: GoField[], data: string) {
686+
// addr and value give a field's address and value within a record.
687+
function generateFieldAccessors(w: CodeWriter, structName: string, fields: GoField[], addr: (field: GoField) => string, value: (field: GoField) => string) {
664688
let wrote = false;
665-
for (const { name, goType, isNode } of fields) {
689+
for (const field of fields) {
690+
const { name, goType, isNode } = field;
666691
if (goType.startsWith("atomic.")) continue;
667-
const storage = goStorageName(name);
668692
const side = sideTableOf(goType);
669693
if (isNode) {
670-
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return node.deref(&${data}.${storage}) }`);
671-
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { node.setRef(&${data}.${storage}, value) }`);
694+
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return node.deref(${addr(field)}) }`);
695+
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { node.setRef(${addr(field)}, value) }`);
672696
}
673697
else if (side) {
674-
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return node.store().${side}.get(${data}.${storage}) }`);
675-
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { node.store().${side}.set(&${data}.${storage}, value, ${zeroCheck(goType, "value")}) }`);
698+
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return node.store().${side}.get(${value(field)}) }`);
699+
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { node.store().${side}.set(${addr(field)}, value, ${zeroCheck(goType, "value")}) }`);
676700
}
677701
else {
678-
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return ${data}.${storage} }`);
679-
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { ${data}.${storage} = value }`);
702+
w.write(`func (node ${structName}) ${getterName(name, goType)}() ${goType} { return ${value(field)} }`);
703+
w.write(`func (node ${structName}) ${setterName(name)}(value ${goType}) { ${value(field)} = value }`);
680704
}
681705
wrote = true;
682706
}
@@ -1106,7 +1130,7 @@ function generateSubtreeFactsDispatch(w: CodeWriter) {
11061130
w.write(`case ${kinds.join(", ")}:`);
11071131
w.push();
11081132
if (transitiveBaseKeys(node).has("CompositeBase")) {
1109-
w.write(`return ${dataOf(node, "n")}.subtreeFactsWorker(n)`);
1133+
w.write(`return subtreeFactsWorker(&${dataOf(node, "n")}.facts, n)`);
11101134
}
11111135
else {
11121136
w.write("return n.computeSubtreeFacts()");

‎tsc/internal/ast/ast.go‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,12 +1520,12 @@ func (node Node) EagerJSDoc(file *SourceFile) []Node {
15201520

15211521
// CompositeBase
15221522

1523-
func (node *compositeBaseData) subtreeFactsWorker(self Node) SubtreeFacts {
1523+
func subtreeFactsWorker(cache *atomic.Uint32, self Node) SubtreeFacts {
15241524
// computeSubtreeFacts() is expected to be idempotent, so races will only impact time, not correctness.
1525-
facts := SubtreeFacts(node.facts.Load())
1525+
facts := SubtreeFacts(cache.Load())
15261526
if facts&SubtreeFactsComputed == 0 {
15271527
facts |= self.computeSubtreeFacts() | SubtreeFactsComputed
1528-
node.facts.Store(uint32(facts))
1528+
cache.Store(uint32(facts))
15291529
}
15301530
return facts &^ SubtreeFactsComputed
15311531
}
@@ -2349,9 +2349,10 @@ type TokenCacheKey struct {
23492349
// itself, giving *SourceFile the shared node methods.
23502350
type SourceFile struct {
23512351
nodeBaseData
2352-
declarationBaseData
2353-
localsContainerBaseData
2354-
compositeBaseData
2352+
nextContainer nodeRef
2353+
symbol sideRef
2354+
locals sideRef
2355+
facts atomic.Uint32
23552356
Node
23562357

23572358
// Fields set by NewSourceFile

0 commit comments

Comments
 (0)