Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
05cd22b
Access node header fields through methods
jakebailey Oct 14, 2026
008783b
Access node fields through generated methods
jakebailey Oct 14, 2026
96a1407
Recognize IsNil guards in the checkchildren lint
jakebailey Oct 15, 2026
0459110
Generalize nil-checking core helpers to comparable types
jakebailey Oct 15, 2026
9026858
Represent AST nodes as value handles
jakebailey Oct 15, 2026
db37d92
Store AST nodes in per-file record tables
jakebailey Oct 15, 2026
0054207
Keep Go pointers out of AST node records
jakebailey Oct 15, 2026
2706845
Store node string fields as text offsets
jakebailey Oct 15, 2026
c61e060
Page node record tables and index sealed layouts directly
jakebailey Oct 15, 2026
434033a
Allocate AST nodes in per-file arenas addressed by pointer
jakebailey Oct 15, 2026
4cc169d
Refer to AST records by address
jakebailey Oct 15, 2026
42dc9fe
Grow AST chunks more slowly
jakebailey Oct 15, 2026
c68646c
Shrink the AST node header to 32 bytes
jakebailey Oct 15, 2026
55a5e79
Flatten AST records and locate base fields by offset table
jakebailey Oct 15, 2026
5ec7bba
Add arena AST design doc
jakebailey Oct 16, 2026
36a9163
Allocate flow nodes in the AST arena
jakebailey Oct 16, 2026
c69a238
Store record refs as 32-bit block offsets
jakebailey Oct 16, 2026
ccd3c0a
Store near refs as offsets from the referring field
jakebailey Oct 16, 2026
3402639
Store records in one indexed array per file
jakebailey Oct 16, 2026
0e4fd24
Bind files as they are parsed, then freeze their records
jakebailey Oct 16, 2026
42abbf1
Store flow nodes as indexed values in each file's store
jakebailey Oct 16, 2026
c2bb293
Store node lists as records in each file's store
jakebailey Oct 16, 2026
81547e8
Iterate node lists in place on hot checker paths
jakebailey Oct 17, 2026
273b301
Cache node kind in the handle
jakebailey Oct 17, 2026
5aab18e
Update ARENA-AST.md with steps 16-17 and current numbers vs main
jakebailey Oct 17, 2026
670ee28
Find a node's source file from its store
jakebailey Oct 17, 2026
0c5d478
Document why check time trails main and the step 18 change
jakebailey Oct 17, 2026
ba2d083
Address nodes by record pointer with self-relative refs
jakebailey Oct 17, 2026
2e5b208
Make a node handle a single pointer
jakebailey Oct 17, 2026
4b85ba3
Size chunks to the allocator's size classes
jakebailey Oct 17, 2026
ca2bc7e
Read shared node members through per-kind offset tables
jakebailey Oct 17, 2026
a4fca73
Make walking up the tree cheap enough to inline
jakebailey Oct 17, 2026
b65f487
Keep hot node accessors within the inlining budget
jakebailey Oct 17, 2026
f6245c4
Locate all node text the same way
jakebailey Oct 17, 2026
86be7af
Follow a near parent with a single add
jakebailey Oct 17, 2026
1e5f65a
Answer absent refs without reading the far slots
jakebailey Oct 17, 2026
67837fc
Inline GetNodeId again
jakebailey Oct 17, 2026
bae744b
Update ARENA-AST.md for the record-address design
jakebailey Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions ARENA-AST.md

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions tools/customlint/checkchildren.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
// conditionally, e.g.
//
// var exprType *Type
// if node.Expression() != nil {
// if !node.Expression().IsNil() {
// exprType = c.checkExpressionCached(node.Expression())
// }
// if c.grammarError(node) {
Expand Down Expand Up @@ -238,9 +238,18 @@ func guardedNil(ret inspector.Cursor, childExpr ast.Expr) bool {
}

// condImpliesNil reports whether `cond` evaluating to `condIsTrue` implies that
// `childExpr` is nil, i.e. `childExpr == nil` (when true) or `childExpr != nil`
// (when false).
// `childExpr` is nil, i.e. `childExpr == nil` or `childExpr.IsNil()` (when
// true) or `childExpr != nil` or `!childExpr.IsNil()` (when false).
func condImpliesNil(cond ast.Expr, childExpr ast.Expr, condIsTrue bool) bool {
if un, ok := cond.(*ast.UnaryExpr); ok && un.Op == token.NOT {
return condImpliesNil(un.X, childExpr, !condIsTrue)
}
if call, ok := cond.(*ast.CallExpr); ok && condIsTrue && len(call.Args) == 0 {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "IsNil" {
return equalExpr(sel.X, childExpr)
}
return false
}
bin, ok := cond.(*ast.BinaryExpr)
if !ok {
return false
Expand Down
14 changes: 14 additions & 0 deletions tools/customlint/testdata/checkchildren/checkchildren.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ type Node struct{}

func (n *Node) Expression() *Node { return nil }

func (n *Node) IsNil() bool { return n == nil }

type Type struct{}

type CheckMode int
Expand Down Expand Up @@ -40,6 +42,8 @@ func (c *Checker) checkExpressionWorker(node *Node, checkMode CheckMode) *Type {
return c.checkCallLike(node)
case node != nil:
return c.checkNilGuardedReturn(node)
case node.IsNil():
return c.checkIsNilGuardedReturn(node)
default:
return c.checkReturnsInReturn(node)
}
Expand Down Expand Up @@ -112,6 +116,16 @@ func (c *Checker) checkNilGuardedReturn(node *Node) *Type {
return c.checkExpression(node.Expression(), 0)
}

// checkIsNilGuardedReturn is like checkNilGuardedReturn, but guards with an
// IsNil method call.
func (c *Checker) checkIsNilGuardedReturn(node *Node) *Type {
c.grammarError(node)
if node.Expression().IsNil() {
return c.errorType
}
return c.checkExpression(node.Expression(), 0)
}

// checkNeverDispatched is not reachable from a worker, so it is never analyzed.
func (c *Checker) checkNeverDispatched(node *Node) *Type {
if c.grammarError(node) {
Expand Down
14 changes: 14 additions & 0 deletions tools/customlint/testdata/checkchildren/checkchildren.go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

func (n *Node) Expression() *Node { return nil }

func (n *Node) IsNil() bool { return n == nil }

type Type struct{}

type CheckMode int
Expand Down Expand Up @@ -40,6 +42,8 @@
return c.checkCallLike(node)
case node != nil:
return c.checkNilGuardedReturn(node)
case node.IsNil():
return c.checkIsNilGuardedReturn(node)
default:
return c.checkReturnsInReturn(node)
}
Expand Down Expand Up @@ -116,6 +120,16 @@
return c.checkExpression(node.Expression(), 0)
}

// checkIsNilGuardedReturn is like checkNilGuardedReturn, but guards with an
// IsNil method call.
func (c *Checker) checkIsNilGuardedReturn(node *Node) *Type {
c.grammarError(node)
if node.Expression().IsNil() {
return c.errorType
}
return c.checkExpression(node.Expression(), 0)
}

// checkNeverDispatched is not reachable from a worker, so it is never analyzed.
func (c *Checker) checkNeverDispatched(node *Node) *Type {
if c.grammarError(node) {
Expand Down
16 changes: 8 additions & 8 deletions tools/scripts/tsc/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@
"goOnly": true
},
"NextContainer": {
"type": "*Node",
"type": "Node",
"goOnly": true
}
},
Expand All @@ -938,7 +938,7 @@
"FlowNodeBase": {
"fields": {
"FlowNode": {
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
},
Expand Down Expand Up @@ -992,7 +992,7 @@
"optional": true
},
"EndFlowNode": {
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
},
Expand Down Expand Up @@ -1543,7 +1543,7 @@
},
{
"name": "FallthroughFlowNode",
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
]
Expand Down Expand Up @@ -1882,7 +1882,7 @@
},
{
"name": "ReturnFlowNode",
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
],
Expand Down Expand Up @@ -2372,7 +2372,7 @@
},
{
"name": "ReturnFlowNode",
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
]
Expand Down Expand Up @@ -2672,7 +2672,7 @@
},
{
"name": "ReturnFlowNode",
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
]
Expand Down Expand Up @@ -2983,7 +2983,7 @@
},
{
"name": "ReturnFlowNode",
"type": "*FlowNode",
"type": "FlowNode",
"goOnly": true
}
]
Expand Down
66 changes: 29 additions & 37 deletions tools/scripts/tsc/generate-encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,14 @@ function goParamName(m: MemberInfo): string {
return m.goParamName();
}

/** Get the Go accessor for a member on a cast struct (e.g., "n.IsTypeOnly", "n.Name()"). */
/** Get the Go accessor for a member on a cast struct (e.g., "n.IsTypeOnly()", "n.Name()"). */
function goFieldAccess(m: MemberInfo): string {
if (m.private) return `n.${api.capitalize(m.name)}()`;
return `n.${m.name}`;
return `n.${api.capitalize(m.name)}()`;
}

/** Get the default Go value for a non-encoded factory parameter. */
function goDefaultValue(m: MemberInfo): string {
if (m.isChild()) return "nil";
if (m.isChild()) return childType(m) === "node" ? "ast.Node{}" : "nil";
const dt = m.declaredType;
if (dt.kind === "primitive") {
switch (dt.name) {
Expand Down Expand Up @@ -366,9 +365,9 @@ function generateGoEncoder(): string {
}

function generateGoGetNodeDataType(w: CodeWriter) {
w.write("func getNodeDataType(node *ast.Node) uint32 {");
w.write("func getNodeDataType(node ast.Node) uint32 {");
w.push();
w.write("switch node.Kind {");
w.write("switch node.Kind() {");

const stringKinds: string[] = [];
const extendedKinds: string[] = [];
Expand Down Expand Up @@ -409,9 +408,9 @@ function generateGoGetNodeDataType(w: CodeWriter) {
}

function generateGoGetChildrenPropertyMask(w: CodeWriter) {
w.write("func getChildrenPropertyMask(node *ast.Node) uint8 {");
w.write("func getChildrenPropertyMask(node ast.Node) uint8 {");
w.push();
w.write("switch node.Kind {");
w.write("switch node.Kind() {");

for (const node of api.nodes()) {
const info = analyzeNode(node);
Expand All @@ -434,13 +433,13 @@ function generateGoGetChildrenPropertyMask(w: CodeWriter) {
check = `hasModifiers(n.Modifiers())`;
}
else if (ct === "nodeList") {
check = `${goFieldAccess(m)} != nil`;
check = `!${goFieldAccess(m)}.IsNil()`;
}
else if (ct === "rawNodeList") {
check = `len(${goFieldAccess(m)}) > 0`;
}
else {
check = `${goFieldAccess(m)} != nil`;
check = `!${goFieldAccess(m)}.IsNil()`;
}
parts.push(`(boolToByte(${check}) << ${i})`);
}
Expand All @@ -459,9 +458,9 @@ function generateGoGetChildrenPropertyMask(w: CodeWriter) {
}

function generateGoGetNodeCommonData(w: CodeWriter) {
w.write("func getNodeCommonData(node *ast.Node) uint32 {");
w.write("func getNodeCommonData(node ast.Node) uint32 {");
w.push();
w.write("switch node.Kind {");
w.write("switch node.Kind() {");

for (const node of api.nodes()) {
const info = analyzeNode(node);
Expand Down Expand Up @@ -525,29 +524,27 @@ function generateGoGetNodeCommonData(w: CodeWriter) {
}

function generateGoRecordNodeStrings(w: CodeWriter) {
w.write("func recordNodeStrings(node *ast.Node, strs *stringTable) uint32 {");
w.write("func recordNodeStrings(node ast.Node, strs *stringTable) uint32 {");
w.push();
w.write("switch node.Kind {");
w.write("switch node.Kind() {");

for (const node of api.nodes()) {
const info = analyzeNode(node);
if (info.dataType !== "string" || !info.textMember) continue;

const kinds = getGoKinds(node);
const castName = goCastName(node);
const textAccess = info.textMember.private
? `node.${castName}().${api.capitalize(info.textMember.name)}()`
: `node.${castName}().${info.textMember.name}`;
const textAccess = `node.${castName}().${api.capitalize(info.textMember.name)}()`;

w.write(`case ${kinds.join(", ")}:`);
w.push();
w.write(`return strs.add(${textAccess}, node.Kind, node.Pos(), node.End())`);
w.write(`return strs.add(${textAccess}, node.Kind(), node.Pos(), node.End())`);
w.pop();
}

w.write("default:");
w.push();
w.write(`panic(fmt.Sprintf("Unexpected node kind %v", node.Kind))`);
w.write(`panic(fmt.Sprintf("Unexpected node kind %v", node.Kind()))`);
w.pop();
w.write("}");
w.pop();
Expand All @@ -556,10 +553,10 @@ function generateGoRecordNodeStrings(w: CodeWriter) {
}

function generateGoRecordExtendedData(w: CodeWriter) {
w.write("func recordExtendedData(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) uint32 {");
w.write("func recordExtendedData(node ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) uint32 {");
w.push();
w.write("offset := uint32(len(*extendedData))");
w.write("switch node.Kind {");
w.write("switch node.Kind() {");

for (const node of api.nodes()) {
const info = analyzeNode(node);
Expand All @@ -575,7 +572,7 @@ function generateGoRecordExtendedData(w: CodeWriter) {

w.write("default:");
w.push();
w.write(`panic(fmt.Sprintf("unknown extended data node kind %v", node.Kind))`);
w.write(`panic(fmt.Sprintf("unknown extended data node kind %v", node.Kind()))`);
w.pop();
w.write("}");
w.write("return offset");
Expand Down Expand Up @@ -611,7 +608,7 @@ function generateGoDecoder(): string {
}

function generateGoCreateStringNode(w: CodeWriter) {
w.write("func (d *astDecoder) createStringNode(kind ast.Kind, data uint32, commonData uint8) (*ast.Node, error) {");
w.write("func (d *astDecoder) createStringNode(kind ast.Kind, data uint32, commonData uint8) (ast.Node, error) {");
w.push();
w.write("strIdx := data & NodeDataStringIndexMask");
w.write("text := d.getString(strIdx)");
Expand Down Expand Up @@ -646,7 +643,7 @@ function generateGoCreateStringNode(w: CodeWriter) {
}
}
else if (isEncodedChild(m)) {
args.push("nil");
args.push(goDefaultValue(m));
}
else {
const v = commonVars.get(m);
Expand All @@ -665,7 +662,7 @@ function generateGoCreateStringNode(w: CodeWriter) {

w.write("default:");
w.push();
w.write(`return nil, fmt.Errorf("unknown string node kind %v", kind)`);
w.write(`return ast.Node{}, fmt.Errorf("unknown string node kind %v", kind)`);
w.pop();
w.write("}");
w.pop();
Expand All @@ -674,7 +671,7 @@ function generateGoCreateStringNode(w: CodeWriter) {
}

function generateGoCreateExtendedNode(w: CodeWriter) {
w.write("func (d *astDecoder) createExtendedNode(kind ast.Kind, data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {");
w.write("func (d *astDecoder) createExtendedNode(kind ast.Kind, data uint32, childIndices []int, commonData uint8) (ast.Node, error) {");
w.push();
w.write("switch kind {");

Expand All @@ -693,7 +690,7 @@ function generateGoCreateExtendedNode(w: CodeWriter) {

w.write("default:");
w.push();
w.write(`return nil, fmt.Errorf("unknown extended data node kind %v", kind)`);
w.write(`return ast.Node{}, fmt.Errorf("unknown extended data node kind %v", kind)`);
w.pop();
w.write("}");
w.pop();
Expand All @@ -702,7 +699,7 @@ function generateGoCreateExtendedNode(w: CodeWriter) {
}

function generateGoCreateChildrenNode(w: CodeWriter) {
w.write("func (d *astDecoder) createChildrenNode(kind ast.Kind, data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {");
w.write("func (d *astDecoder) createChildrenNode(kind ast.Kind, data uint32, childIndices []int, commonData uint8) (ast.Node, error) {");
w.push();
w.write("mask := uint8(data & NodeDataChildMask)");
w.write("");
Expand Down Expand Up @@ -752,7 +749,7 @@ function generateGoCreateChildrenNode(w: CodeWriter) {
// Default: error for unhandled kinds
w.write("default:");
w.push();
w.write(`return nil, fmt.Errorf("unhandled node kind %v with %d children", kind, len(childIndices))`);
w.write(`return ast.Node{}, fmt.Errorf("unhandled node kind %v with %d children", kind, len(childIndices))`);
w.pop();

w.write("}");
Expand Down Expand Up @@ -895,7 +892,7 @@ function emitSingleChildDecoder(w: CodeWriter, node: NodeType, info: EncoderNode
}
else if (ct === "nodeList") {
if (hasCommonData) {
w.write("var list *ast.NodeList");
w.write("var list ast.NodeList");
w.write("if len(childIndices) > 0 {");
w.push();
w.write("list = d.nodeListAt(childIndices[0])");
Expand All @@ -908,7 +905,7 @@ function emitSingleChildDecoder(w: CodeWriter, node: NodeType, info: EncoderNode
}
}
else if (ct === "modifierList") {
w.write("var mods *ast.ModifierList");
w.write("var mods ast.ModifierList");
w.write("if len(childIndices) > 0 {");
w.push();
w.write("mods = d.modifierListAt(childIndices[0])");
Expand Down Expand Up @@ -960,12 +957,7 @@ function emitMultiChildDecoder(w: CodeWriter, node: NodeType, info: EncoderNodeI
else if (ct === "rawNodeList") {
const nlVar = `${varName}NL`;
w.write(`${nlVar} := d.nodeListAt(it.nextIf(mask, ${i}))`);
w.write(`var ${varName} []*ast.Node`);
w.write(`if ${nlVar} != nil {`);
w.push();
w.write(`${varName} = ${nlVar}.Nodes`);
w.pop();
w.write("}");
w.write(`${varName} := ${nlVar}.Slice()`);
childVars.set(m, varName);
continue;
}
Expand Down
Loading