From 2dfa95008c046152ef677bc5d06ea67fff93fc12 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:03:41 -0700 Subject: [PATCH 1/3] Keep optional symbol tables out of hot symbol records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most symbols do not need members, exports, or an export-symbol link. Keeping all three pointers in every symbol's shared metadata charges every property, parameter, and synthetic symbol for those features. Move them into optional shared metadata, reducing SymbolWithData from 104 to 88 bytes on amd64. Binder- and checker-owned typed arenas batch the sidecars without adding an allocator pointer to every symbol. Shared symbolData still sees sidecars created through either alias. Compared with the parent commit, which already includes the checker storage and direct-header AST changes. Six fresh-process interleaved samples parse, bind, and check ../vscode/src with the same single checker and normal GC settings. Both workloads report 321 diagnostics, 10284 files. Exact CLI diagnostics and compiler counts also match. CPU timings are noisy. Hardware instructions and allocation counts show no statistically established difference; do not claim CPU gains. goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 4.180Gi ± 0% 4.129Gi ± 0% -1.21% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 3.738Gi ± 0% 3.687Gi ± 0% -1.34% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 5.659Gi ± 2% 5.606Gi ± 0% -0.93% (p=0.002 n=6) │ vscode-symbol-batched-before.txt │ vscode-symbol-batched.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 26.06M ± 2% 26.08M ± 0% ~ (p=0.310 n=6) │ symbol-batched-before-instructions.txt │ symbol-batched-instructions.txt │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 302.1G ± 4% 299.9G ± 3% ~ (p=0.589 n=6) --- tsc/internal/ast/symbol.go | 69 +++++++++++++++++++++++---- tsc/internal/ast/symbol_test.go | 83 +++++++++++++++++++++++++++------ tsc/internal/ast/utilities.go | 8 ++-- tsc/internal/binder/binder.go | 47 ++++++++++--------- tsc/internal/checker/checker.go | 21 +++++---- 5 files changed, 168 insertions(+), 60 deletions(-) diff --git a/tsc/internal/ast/symbol.go b/tsc/internal/ast/symbol.go index 95cb4f2154d27..00312310691c5 100644 --- a/tsc/internal/ast/symbol.go +++ b/tsc/internal/ast/symbol.go @@ -4,6 +4,7 @@ import ( "strings" "sync/atomic" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/debug" ) @@ -23,10 +24,20 @@ type symbolData struct { name string declarations []*Node valueDeclaration *Node - members SymbolTable - exports SymbolTable parent *Symbol - exportSymbol *Symbol + extra *symbolExtra +} + +type symbolExtra struct { + members SymbolTable + exports SymbolTable + exportSymbol *Symbol +} + +// SymbolExtraArena batches optional symbol metadata without adding an allocator +// pointer to every symbol. References from symbolData keep its blocks alive. +type SymbolExtraArena struct { + arena core.Arena[symbolExtra] } func (s *Symbol) Flags() SymbolFlags { return s.flags } @@ -34,20 +45,60 @@ func (s *Symbol) CheckFlags() CheckFlags { return s.checkFlags } func (s *Symbol) Name() string { return s.data.name } func (s *Symbol) Declarations() []*Node { return s.data.declarations } func (s *Symbol) ValueDeclaration() *Node { return s.data.valueDeclaration } -func (s *Symbol) Members() SymbolTable { return s.data.members } -func (s *Symbol) Exports() SymbolTable { return s.data.exports } func (s *Symbol) Parent() *Symbol { return s.data.parent } -func (s *Symbol) ExportSymbol() *Symbol { return s.data.exportSymbol } + +func (s *Symbol) Members() SymbolTable { + if s.data.extra == nil { + return nil + } + return s.data.extra.members +} + +func (s *Symbol) Exports() SymbolTable { + if s.data.extra == nil { + return nil + } + return s.data.extra.exports +} + +func (s *Symbol) ExportSymbol() *Symbol { + if s.data.extra == nil { + return nil + } + return s.data.extra.exportSymbol +} func (s *Symbol) SetFlags(value SymbolFlags) { s.flags = value } func (s *Symbol) SetCheckFlags(value CheckFlags) { s.checkFlags = value } func (s *Symbol) SetName(value string) { s.data.name = value } func (s *Symbol) SetDeclarations(value []*Node) { s.data.declarations = value } func (s *Symbol) SetValueDeclaration(value *Node) { s.data.valueDeclaration = value } -func (s *Symbol) SetMembers(value SymbolTable) { s.data.members = value } -func (s *Symbol) SetExports(value SymbolTable) { s.data.exports = value } func (s *Symbol) SetParent(value *Symbol) { s.data.parent = value } -func (s *Symbol) SetExportSymbol(value *Symbol) { s.data.exportSymbol = value } + +func (s *Symbol) ensureExtra(arena *SymbolExtraArena) *symbolExtra { + if s.data.extra == nil { + s.data.extra = arena.arena.New() + } + return s.data.extra +} + +func (s *Symbol) SetMembers(value SymbolTable, arena *SymbolExtraArena) { + if value != nil || s.data.extra != nil { + s.ensureExtra(arena).members = value + } +} + +func (s *Symbol) SetExports(value SymbolTable, arena *SymbolExtraArena) { + if value != nil || s.data.extra != nil { + s.ensureExtra(arena).exports = value + } +} + +func (s *Symbol) SetExportSymbol(value *Symbol, arena *SymbolExtraArena) { + if value != nil || s.data.extra != nil { + s.ensureExtra(arena).exportSymbol = value + } +} // SymbolWithData is a helper structure that contains both a Symbol and its associated symbolData. type SymbolWithData struct { diff --git a/tsc/internal/ast/symbol_test.go b/tsc/internal/ast/symbol_test.go index 3c1997e352b20..1f8f44aaa66d2 100644 --- a/tsc/internal/ast/symbol_test.go +++ b/tsc/internal/ast/symbol_test.go @@ -2,7 +2,9 @@ package ast_test import ( "reflect" + "runtime" "testing" + "unsafe" "github.com/microsoft/TypeScript/tsc/internal/ast" "gotest.tools/v3/assert" @@ -17,6 +19,7 @@ func TestSymbolFieldsArePrivate(t *testing.T) { func TestSymbolAccessors(t *testing.T) { t.Parallel() + var arena ast.SymbolExtraArena symbol := ast.NewSymbol() assert.Equal(t, symbol.Flags(), ast.SymbolFlagsNone) assert.Equal(t, symbol.CheckFlags(), ast.CheckFlags(0)) @@ -43,10 +46,10 @@ func TestSymbolAccessors(t *testing.T) { symbol.SetName("C") symbol.SetDeclarations(declarations) symbol.SetValueDeclaration(declaration) - symbol.SetMembers(members) - symbol.SetExports(exports) + symbol.SetMembers(members, &arena) + symbol.SetExports(exports, &arena) symbol.SetParent(parent) - symbol.SetExportSymbol(exportSymbol) + symbol.SetExportSymbol(exportSymbol, &arena) assert.Equal(t, symbol.Flags(), ast.SymbolFlagsClass|ast.SymbolFlagsTransient) assert.Equal(t, symbol.CheckFlags(), ast.CheckFlagsReadonly) @@ -72,10 +75,10 @@ func TestSymbolAccessors(t *testing.T) { symbol.SetName("") symbol.SetDeclarations(nil) symbol.SetValueDeclaration(nil) - symbol.SetMembers(nil) - symbol.SetExports(nil) + symbol.SetMembers(nil, &arena) + symbol.SetExports(nil, &arena) symbol.SetParent(nil) - symbol.SetExportSymbol(nil) + symbol.SetExportSymbol(nil, &arena) assert.Equal(t, symbol.Flags(), ast.SymbolFlagsNone) assert.Equal(t, symbol.CheckFlags(), ast.CheckFlags(0)) @@ -90,24 +93,76 @@ func TestSymbolAccessors(t *testing.T) { func TestSymbolTableInitialization(t *testing.T) { t.Parallel() + var arena ast.SymbolExtraArena symbol := ast.NewSymbol() member := ast.NewSymbol() export := ast.NewSymbol() - members := ast.GetMembers(symbol) + members := ast.GetMembers(symbol, &arena) members["member"] = member assert.Equal(t, symbol.Members()["member"], member) - assert.Equal(t, ast.GetMembers(symbol)["member"], member) + assert.Equal(t, ast.GetMembers(symbol, &arena)["member"], member) - exports := ast.GetExports(symbol) + exports := ast.GetExports(symbol, &arena) exports["export"] = export assert.Equal(t, symbol.Exports()["export"], export) - assert.Equal(t, ast.GetExports(symbol)["export"], export) + assert.Equal(t, ast.GetExports(symbol, &arena)["export"], export) - symbol.SetMembers(nil) - symbol.SetExports(nil) - assert.Assert(t, ast.GetMembers(symbol) != nil) - assert.Assert(t, ast.GetExports(symbol) != nil) + symbol.SetMembers(nil, &arena) + symbol.SetExports(nil, &arena) + assert.Assert(t, ast.GetMembers(symbol, &arena) != nil) + assert.Assert(t, ast.GetExports(symbol, &arena) != nil) assert.Equal(t, len(symbol.Members()), 0) assert.Equal(t, len(symbol.Exports()), 0) } + +func TestSymbolHotRecordLayout(t *testing.T) { + t.Parallel() + assert.Equal(t, unsafe.Sizeof(ast.SymbolWithData{}), unsafe.Sizeof(ast.Symbol{})+8*unsafe.Sizeof((*ast.Node)(nil))) +} + +func TestSymbolNilExtraFieldsDoNotAllocate(t *testing.T) { //nolint:paralleltest + var arena ast.SymbolExtraArena + symbol := ast.NewSymbol() + allocs := testing.AllocsPerRun(10, func() { + symbol.SetMembers(nil, &arena) + symbol.SetExports(nil, &arena) + symbol.SetExportSymbol(nil, &arena) + }) + assert.Equal(t, allocs, float64(0)) + assert.Assert(t, reflect.ValueOf(symbol).Elem().FieldByName("data").Elem().FieldByName("extra").IsNil()) +} + +func TestSymbolSharedExtraData(t *testing.T) { + t.Parallel() + var arena ast.SymbolExtraArena + var otherArena ast.SymbolExtraArena + symbol := ast.NewSymbol() + alias := ast.NewSymbol() + alias.SetSymbolData(symbol) + member := ast.NewSymbol() + export := ast.NewSymbol() + alias.SetMembers(ast.SymbolTable{"member": member}, &arena) + symbol.SetExports(ast.SymbolTable{"export": export}, &otherArena) + alias.SetExportSymbol(export, &otherArena) + runtime.GC() + assert.Equal(t, symbol.Members()["member"], member) + assert.Equal(t, alias.Exports()["export"], export) + assert.Equal(t, symbol.ExportSymbol(), export) + symbol.SetMembers(nil, &otherArena) + alias.SetExports(nil, &arena) + symbol.SetExportSymbol(nil, &arena) + assert.Assert(t, alias.Members() == nil) + assert.Assert(t, symbol.Exports() == nil) + assert.Assert(t, alias.ExportSymbol() == nil) + ast.GetMembers(alias, &otherArena)["new"] = export + ast.GetExports(symbol, &arena)["new"] = member + for range 1024 { + ast.NewSymbol().SetExports(ast.SymbolTable{"export": export}, &arena) + } + arena = ast.SymbolExtraArena{} + otherArena = ast.SymbolExtraArena{} + runtime.GC() + assert.Equal(t, symbol.Members()["new"], export) + assert.Equal(t, alias.Exports()["new"], member) +} diff --git a/tsc/internal/ast/utilities.go b/tsc/internal/ast/utilities.go index 668883c00d285..ff11fd6a2c2f4 100644 --- a/tsc/internal/ast/utilities.go +++ b/tsc/internal/ast/utilities.go @@ -99,12 +99,12 @@ func GetSymbolTable(data *SymbolTable) SymbolTable { return *data } -func GetMembers(symbol *Symbol) SymbolTable { - return GetSymbolTable(&symbol.data.members) +func GetMembers(symbol *Symbol, arena *SymbolExtraArena) SymbolTable { + return GetSymbolTable(&symbol.ensureExtra(arena).members) } -func GetExports(symbol *Symbol) SymbolTable { - return GetSymbolTable(&symbol.data.exports) +func GetExports(symbol *Symbol, arena *SymbolExtraArena) SymbolTable { + return GetSymbolTable(&symbol.ensureExtra(arena).exports) } func GetLocals(container *Node) SymbolTable { diff --git a/tsc/internal/binder/binder.go b/tsc/internal/binder/binder.go index eb9bcebe5cf35..a95bb19ecdff7 100644 --- a/tsc/internal/binder/binder.go +++ b/tsc/internal/binder/binder.go @@ -74,6 +74,7 @@ type Binder struct { symbolCount int notConstEnumOnlyModules collections.Set[*ast.Symbol] symbolWithDataArena core.Arena[ast.SymbolWithData] + symbolExtraArena ast.SymbolExtraArena flowNodeArena core.Arena[ast.FlowNode] flowListArena core.Arena[ast.FlowList] singleDeclarationsArena core.Arena[*ast.Node] @@ -379,7 +380,7 @@ func (b *Binder) declareModuleMember(node *ast.Node, symbolFlags ast.SymbolFlags hasExportModifier := ast.GetCombinedModifierFlags(node)&ast.ModifierFlagsExport != 0 || ast.IsImplicitlyExportedJSDocDeclaration(node) if symbolFlags&ast.SymbolFlagsAlias != 0 { if node.Kind == ast.KindExportSpecifier || (node.Kind == ast.KindImportEqualsDeclaration && hasExportModifier) { - return b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, symbolFlags, symbolExcludes) } return b.declareSymbol(ast.GetLocals(container), nil /*parent*/, node, symbolFlags, symbolExcludes) } @@ -400,7 +401,7 @@ func (b *Binder) declareModuleMember(node *ast.Node, symbolFlags ast.SymbolFlags // and should never be merged directly with other augmentation, and the latter case would be possible if automatic merge is allowed. if !ast.IsAmbientModule(node) && (hasExportModifier || container.Flags&ast.NodeFlagsExportContext != 0) { if !ast.IsLocalsContainer(container) || (ast.HasSyntacticModifier(node, ast.ModifierFlagsDefault) && b.getDeclarationName(node) == ast.InternalSymbolNameMissing) { - return b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, symbolFlags, symbolExcludes) // No local symbol for an unnamed default! } exportKind := ast.SymbolFlagsNone @@ -408,7 +409,7 @@ func (b *Binder) declareModuleMember(node *ast.Node, symbolFlags ast.SymbolFlags exportKind = ast.SymbolFlagsExportValue } local := b.declareSymbol(ast.GetLocals(container), nil /*parent*/, node, exportKind, symbolExcludes) - local.SetExportSymbol(b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, symbolFlags, symbolExcludes)) + local.SetExportSymbol(b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, symbolFlags, symbolExcludes), &b.symbolExtraArena) node.ExportableData().LocalSymbol = local return local } @@ -417,9 +418,9 @@ func (b *Binder) declareModuleMember(node *ast.Node, symbolFlags ast.SymbolFlags func (b *Binder) declareClassMember(node *ast.Node, symbolFlags ast.SymbolFlags, symbolExcludes ast.SymbolFlags) *ast.Symbol { if ast.IsStatic(node) { - return b.declareSymbol(ast.GetExports(b.container.Symbol()), b.container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetExports(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), node, symbolFlags, symbolExcludes) } - return b.declareSymbol(ast.GetMembers(b.container.Symbol()), b.container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetMembers(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), node, symbolFlags, symbolExcludes) } func (b *Binder) declareSourceFileMember(node *ast.Node, symbolFlags ast.SymbolFlags, symbolExcludes ast.SymbolFlags) *ast.Symbol { @@ -438,9 +439,9 @@ func (b *Binder) declareSymbolAndAddToSymbolTable(node *ast.Node, symbolFlags as case ast.KindClassExpression, ast.KindClassDeclaration: return b.declareClassMember(node, symbolFlags, symbolExcludes) case ast.KindEnumDeclaration: - return b.declareSymbol(ast.GetExports(b.container.Symbol()), b.container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetExports(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), node, symbolFlags, symbolExcludes) case ast.KindTypeLiteral, ast.KindObjectLiteralExpression, ast.KindInterfaceDeclaration, ast.KindJsxAttributes: - return b.declareSymbol(ast.GetMembers(b.container.Symbol()), b.container.Symbol(), node, symbolFlags, symbolExcludes) + return b.declareSymbol(ast.GetMembers(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), node, symbolFlags, symbolExcludes) case ast.KindFunctionType, ast.KindConstructorType, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature, ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor, ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, @@ -763,7 +764,7 @@ func (b *Binder) bindSourceFileIfExternalModule() { b.bindSourceFileAsExternalModule() // Create symbol equivalent for the module.exports = {} originalSymbol := b.file.Symbol - b.declareSymbol(ast.GetExports(b.file.Symbol), b.file.Symbol, b.file.AsNode(), ast.SymbolFlagsProperty, ast.SymbolFlagsAll) + b.declareSymbol(ast.GetExports(b.file.Symbol, &b.symbolExtraArena), b.file.Symbol, b.file.AsNode(), ast.SymbolFlagsProperty, ast.SymbolFlagsAll) b.file.Symbol = originalSymbol } } @@ -853,9 +854,9 @@ func (b *Binder) bindExportDeclaration(node *ast.Node) { b.bindAnonymousDeclaration(node, ast.SymbolFlagsExportStar, b.getDeclarationName(node)) } else if decl.ExportClause == nil { // All export * declarations are collected in an __export symbol - b.declareSymbol(ast.GetExports(b.container.Symbol()), b.container.Symbol(), node, ast.SymbolFlagsExportStar, ast.SymbolFlagsNone) + b.declareSymbol(ast.GetExports(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), node, ast.SymbolFlagsExportStar, ast.SymbolFlagsNone) } else if ast.IsNamespaceExport(decl.ExportClause) { - b.declareSymbol(ast.GetExports(b.container.Symbol()), b.container.Symbol(), decl.ExportClause, ast.SymbolFlagsAlias, ast.SymbolFlagsAliasExcludes) + b.declareSymbol(ast.GetExports(b.container.Symbol(), &b.symbolExtraArena), b.container.Symbol(), decl.ExportClause, ast.SymbolFlagsAlias, ast.SymbolFlagsAliasExcludes) } } @@ -868,7 +869,7 @@ func (b *Binder) bindExportAssignment(node *ast.Node) { // If there is an `export default x;` alias declaration, can't `export default` anything else. // (In contrast, you can still have `export default function f() {}` and `export default interface I {}`.) flags := core.IfElse(ast.ExpressionIsAlias(node.Expression()), ast.SymbolFlagsAlias, ast.SymbolFlagsProperty) - symbol := b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, flags, ast.SymbolFlagsAll) + symbol := b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, flags, ast.SymbolFlagsAll) if node.AsExportAssignment().IsExportEquals { // Ensure export assignments have a ValueDeclaration set. SetValueDeclaration(symbol, node) @@ -967,11 +968,11 @@ func (b *Binder) bindClassLikeDeclaration(node *ast.Node) { // module might have an exported variable called 'prototype'. We can't allow that as // that would clash with the built-in 'prototype' for the class. prototypeSymbol := b.newSymbol(ast.SymbolFlagsProperty|ast.SymbolFlagsPrototype, "prototype") - symbolExport := ast.GetExports(symbol)[prototypeSymbol.Name()] + symbolExport := ast.GetExports(symbol, &b.symbolExtraArena)[prototypeSymbol.Name()] if symbolExport != nil { b.errorOnNode(symbolExport.Declarations()[0], diagnostics.Duplicate_identifier_0, ast.SymbolName(prototypeSymbol)) } - ast.GetExports(symbol)[prototypeSymbol.Name()] = prototypeSymbol + ast.GetExports(symbol, &b.symbolExtraArena)[prototypeSymbol.Name()] = prototypeSymbol prototypeSymbol.SetParent(symbol) } @@ -1000,12 +1001,12 @@ func (b *Binder) bindFunctionOrConstructorType(node *ast.Node) { b.addDeclarationToSymbol(symbol, node, ast.SymbolFlagsSignature) typeLiteralSymbol := b.newSymbol(ast.SymbolFlagsTypeLiteral, ast.InternalSymbolNameType) b.addDeclarationToSymbol(typeLiteralSymbol, node, ast.SymbolFlagsTypeLiteral) - typeLiteralSymbol.SetMembers(make(ast.SymbolTable)) + typeLiteralSymbol.SetMembers(make(ast.SymbolTable), &b.symbolExtraArena) typeLiteralSymbol.Members()[symbol.Name()] = symbol } func (b *Binder) addLateBoundAssignmentDeclarationToSymbol(node *ast.Node, symbol *ast.Symbol) { - exports := ast.GetExports(symbol) + exports := ast.GetExports(symbol, &b.symbolExtraArena) assignmentSymbol := exports[ast.InternalSymbolNameAssignmentDeclaration] if assignmentSymbol == nil { assignmentSymbol = b.newSymbol(ast.SymbolFlagsNone, ast.InternalSymbolNameAssignmentDeclaration) @@ -1018,7 +1019,7 @@ func (b *Binder) bindModuleExportsAssignment(node *ast.Node) { if b.setCommonJSModuleIndicator(node) { container := b.file.AsNode() flags := core.IfElse(ast.ExpressionIsAlias(node.AsBinaryExpression().Right), ast.SymbolFlagsAlias, ast.SymbolFlagsProperty) - symbol := b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, flags, 0) + symbol := b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, flags, 0) SetValueDeclaration(symbol, node) } } @@ -1047,7 +1048,7 @@ func (b *Binder) bindCommonJSTypeExports(moduleSymbol *ast.Symbol) { if exportEquals := moduleExports[ast.InternalSymbolNameExportEquals]; exportEquals != nil { for _, symbol := range moduleExports { if symbol.Name() != ast.InternalSymbolNameExportEquals && symbol.Flags()&(ast.SymbolFlagsType|ast.SymbolFlagsNamespace) != 0 { - ast.GetExports(exportEquals)[symbol.Name()] = symbol + ast.GetExports(exportEquals, &b.symbolExtraArena)[symbol.Name()] = symbol exportEquals.SetFlags(exportEquals.Flags() | ast.SymbolFlagsNamespaceModule) } } @@ -1066,7 +1067,7 @@ func (b *Binder) bindDeferredExpandoAssignment(node *ast.Node) { b.addLateBoundAssignmentDeclarationToSymbol(node, symbol) } else { // We declare expandos only when there are no non-expando declarations for that name. - exports := ast.GetExports(symbol) + exports := ast.GetExports(symbol, &b.symbolExtraArena) if existing := exports[b.getDeclarationName(node)]; existing == nil || existing.Flags()&ast.SymbolFlagsAssignment != 0 { b.declareSymbol(exports, symbol, node, ast.SymbolFlagsProperty|ast.SymbolFlagsAssignment, ast.SymbolFlagsPropertyExcludes) } @@ -1088,7 +1089,7 @@ func (b *Binder) bindExportsOrObjectDefineProperty(node *ast.Node) { if b.setCommonJSModuleIndicator(node) { container := b.file.AsNode() flags := core.IfElse(ast.IsBinaryExpression(node) && ast.ExpressionIsAlias(node.AsBinaryExpression().Right), ast.SymbolFlagsAlias, ast.SymbolFlagsFunctionScopedVariable) - b.declareSymbol(ast.GetExports(container.Symbol()), container.Symbol(), node, flags, ast.SymbolFlagsFunctionScopedVariableExcludes) + b.declareSymbol(ast.GetExports(container.Symbol(), &b.symbolExtraArena), container.Symbol(), node, flags, ast.SymbolFlagsFunctionScopedVariableExcludes) } } @@ -1152,9 +1153,9 @@ func (b *Binder) getThisClassAndSymbolTable() (classSymbol *ast.Symbol, symbolTa // this.property assignment in class member -- bind to the containing class classSymbol = b.thisContainer.Parent.Symbol() if ast.IsStatic(b.thisContainer) { - symbolTable = ast.GetExports(classSymbol) + symbolTable = ast.GetExports(classSymbol, &b.symbolExtraArena) } else { - symbolTable = ast.GetMembers(classSymbol) + symbolTable = ast.GetMembers(classSymbol, &b.symbolExtraArena) } } return classSymbol, symbolTable @@ -1211,7 +1212,7 @@ func (b *Binder) bindParameter(node *ast.Node) { if ast.IsParameterPropertyDeclaration(node, node.Parent) { classDeclaration := node.Parent.Parent flags := ast.SymbolFlagsProperty | core.IfElse(decl.QuestionToken != nil, ast.SymbolFlagsOptional, ast.SymbolFlagsNone) - b.declareSymbol(ast.GetMembers(classDeclaration.Symbol()), classDeclaration.Symbol(), node, flags, ast.SymbolFlagsPropertyExcludes) + b.declareSymbol(ast.GetMembers(classDeclaration.Symbol(), &b.symbolExtraArena), classDeclaration.Symbol(), node, flags, ast.SymbolFlagsPropertyExcludes) } } @@ -1635,7 +1636,7 @@ func (b *Binder) declareCommonJSVariable(name string) { exportsProperty.SetDeclarations(symbol.Declarations()) exportsProperty.SetValueDeclaration(symbol.ValueDeclaration()) exportsProperty.SetParent(symbol) - symbol.SetMembers(make(ast.SymbolTable, 1)) + symbol.SetMembers(make(ast.SymbolTable, 1), &b.symbolExtraArena) symbol.Members()["exports"] = exportsProperty } locals[name] = symbol diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 6bf62110a9b9b..0ed0f2e206a27 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -672,6 +672,7 @@ type Checker struct { suggestionDiagnostics ast.DiagnosticsCollection symbolArena core.Arena[ast.Symbol] symbolWithDataArena core.Arena[ast.SymbolWithData] + symbolExtraArena ast.SymbolExtraArena signatureArena core.Arena[Signature] indexInfoArena core.Arena[IndexInfo] literalTypeArena core.Arena[LiteralType] @@ -989,7 +990,7 @@ func NewChecker(program Program, tracer *Tracer) (*Checker, *sync.Mutex) { c.errorTypes = make(map[CacheHashKey]*Type) c.moduleSymbols = make(map[*ast.Node]*ast.Symbol) c.globalThisSymbol = c.newSymbolEx(ast.SymbolFlagsModule, "globalThis", ast.CheckFlagsReadonly) - c.globalThisSymbol.SetExports(c.globals) + c.globalThisSymbol.SetExports(c.globals, &c.symbolExtraArena) c.globals[c.globalThisSymbol.Name()] = c.globalThisSymbol c.resolveName = c.createNameResolver().Resolve c.resolveNameForSymbolSuggestion = c.createNameResolverForSuggestion().Resolve @@ -14474,10 +14475,10 @@ func (c *Checker) mergeSymbol(target *ast.Symbol, source *ast.Symbol, unidirecti } target.SetDeclarations(append(target.Declarations(), source.Declarations()...)) if source.Members() != nil { - c.mergeSymbolTable(ast.GetMembers(target), source.Members(), unidirectional, nil) + c.mergeSymbolTable(ast.GetMembers(target, &c.symbolExtraArena), source.Members(), unidirectional, nil) } if source.Exports() != nil { - c.mergeSymbolTable(ast.GetExports(target), source.Exports(), unidirectional, target) + c.mergeSymbolTable(ast.GetExports(target, &c.symbolExtraArena), source.Exports(), unidirectional, target) } if !unidirectional { c.recordMergedSymbol(target, source) @@ -14637,8 +14638,8 @@ func (c *Checker) cloneSymbol(symbol *ast.Symbol) *ast.Symbol { result.SetDeclarations(symbol.Declarations()[0:len(symbol.Declarations()):len(symbol.Declarations())]) result.SetParent(symbol.Parent()) result.SetValueDeclaration(symbol.ValueDeclaration()) - result.SetMembers(maps.Clone(symbol.Members())) - result.SetExports(maps.Clone(symbol.Exports())) + result.SetMembers(maps.Clone(symbol.Members()), &c.symbolExtraArena) + result.SetExports(maps.Clone(symbol.Exports()), &c.symbolExtraArena) c.recordMergedSymbol(result, symbol) return result } @@ -15101,8 +15102,8 @@ func (c *Checker) combineValueAndTypeSymbols(valueSymbol *ast.Symbol, typeSymbol result.SetParent(typeSymbol.Parent()) } result.SetValueDeclaration(valueSymbol.ValueDeclaration()) - result.SetMembers(maps.Clone(typeSymbol.Members())) - result.SetExports(maps.Clone(valueSymbol.Exports())) + result.SetMembers(maps.Clone(typeSymbol.Members()), &c.symbolExtraArena) + result.SetExports(maps.Clone(valueSymbol.Exports()), &c.symbolExtraArena) return result } @@ -16151,8 +16152,8 @@ func (c *Checker) cloneTypeAsModuleType(symbol *ast.Symbol, moduleType *Type, re result := c.newSymbol(symbol.Flags(), symbol.Name()) result.SetDeclarations(slices.Clone(symbol.Declarations())) result.SetValueDeclaration(symbol.ValueDeclaration()) - result.SetMembers(maps.Clone(symbol.Members())) - result.SetExports(maps.Clone(symbol.Exports())) + result.SetMembers(maps.Clone(symbol.Members()), &c.symbolExtraArena) + result.SetExports(maps.Clone(symbol.Exports()), &c.symbolExtraArena) result.SetParent(symbol.Parent()) links := c.exportTypeLinks.Get(result) links.target = symbol @@ -25268,7 +25269,7 @@ func (c *Checker) getGlobalImportMetaExpressionType() *Type { metaPropertySymbol.SetParent(symbol) c.valueSymbolLinks.Get(metaPropertySymbol).resolvedType = importMetaType members := createSymbolTable([]*ast.Symbol{metaPropertySymbol}) - symbol.SetMembers(members) + symbol.SetMembers(members, &c.symbolExtraArena) c.deferredGlobalImportMetaExpressionType = c.newAnonymousType(symbol, members, nil, nil, nil) } return c.deferredGlobalImportMetaExpressionType From 79afb4bd60f539d2f152c1ae79f944d984e4a469 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:45:44 -0700 Subject: [PATCH 2/3] Move optional structured type shape into cold storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every object, union, and intersection paid for signature and index-info slice headers, a call-signature count, and a node-builder cache, even when none were needed. Those fields made StructuredType 144 bytes on amd64. Keep them in an optional checker-arena sidecar and reduce the hot record to 88 bytes. Preserve signature publication during recursive anonymous resolution, clipped public signature slices, index-info nil/empty distinctions, and independent node-builder cache identity. Empty unresolved shape does not allocate a sidecar. Compared with the parent commit, including all previous checker, AST, and symbol reductions. Six fresh-process interleaved samples parse, bind, and check the actual VS Code project using one checker and normal GC settings. Live heap falls another 1.81%, GC scan bytes fall 2.01%, and allocated bytes fall 1.28%. Allocation counts do not differ significantly. Neither hardware instructions nor elapsed time establishes a change; this does not claim a CPU improvement. Exact CLI diagnostic text and compiler counts match the baseline. Exact benchstat output: goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-shape-before.txt │ vscode-shape.txt │ │ sec/op │ sec/op vs base │ ExternalProgram-20 41.55 ± 6% 41.84 ± 13% ~ (p=0.937 n=6) │ vscode-shape-before.txt │ vscode-shape.txt │ │ diagnostics │ diagnostics vs base │ ExternalProgram-20 321.0 ± 0% 321.0 ± 0% ~ (p=1.000 n=6) ¹ ¹ all samples are equal │ vscode-shape-before.txt │ vscode-shape.txt │ │ files │ files vs base │ ExternalProgram-20 10.28k ± 0% 10.28k ± 0% ~ (p=1.000 n=6) ¹ ¹ all samples are equal │ vscode-shape-before.txt │ vscode-shape.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 3.959Gi ± 0% 3.887Gi ± 0% -1.81% (p=0.002 n=6) │ vscode-shape-before.txt │ vscode-shape.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 3.553Gi ± 0% 3.482Gi ± 0% -2.01% (p=0.002 n=6) │ vscode-shape-before.txt │ vscode-shape.txt │ │ symbols │ symbols vs base │ ExternalProgram-20 4.154M ± 0% 4.154M ± 0% ~ (p=1.000 n=6) ¹ ¹ all samples are equal │ vscode-shape-before.txt │ vscode-shape.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 5.577Gi ± 0% 5.506Gi ± 0% -1.28% (p=0.002 n=6) │ vscode-shape-before.txt │ vscode-shape.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 27.36M ± 0% 27.35M ± 0% ~ (p=0.818 n=6) │ shape-before-instructions.txt │ shape-instructions.txt │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 292.5G ± 4% 301.6G ± 4% ~ (p=0.093 n=6) --- tsc/internal/checker/checker.go | 65 ++++++++++++++++--------- tsc/internal/checker/nodebuilderimpl.go | 18 +++---- tsc/internal/checker/relater.go | 2 +- tsc/internal/checker/types.go | 39 +++++++++++++-- tsc/internal/checker/types_test.go | 50 +++++++++++++++++++ 5 files changed, 138 insertions(+), 36 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 0ed0f2e206a27..ffa9629091a35 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -687,6 +687,7 @@ type Checker struct { conditionalTypeArena core.Arena[ConditionalType] typeListArena core.Arena[*Type] valueSymbolExtraArena core.Arena[valueSymbolExtra] + structuredTypeExtraArena core.Arena[structuredTypeExtra] freeTypeSets *temporaryTypeSet mergedSymbols map[*ast.Symbol]*ast.Symbol mergedExportsChecked collections.Set[*ast.Symbol] @@ -4606,7 +4607,7 @@ func (c *Checker) getTypeWithoutSignatures(t *Type) *Type { switch { case t.flags&TypeFlagsObject != 0: resolved := c.resolveStructuredTypeMembers(t) - if len(resolved.signatures) != 0 { + if len(resolved.getSignatures()) != 0 { result := c.newObjectType(ObjectFlagsAnonymous, t.symbol) result.objectFlags |= ObjectFlagsMembersResolved result.AsObjectType().members = resolved.members @@ -10947,7 +10948,7 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node) debug.Assert(t.symbol != nil, "Instantiation expression source type must have a symbol") symbol.SetDeclarations(t.symbol.Declarations()) result := c.newObjectType(ObjectFlagsAnonymous|ObjectFlagsInstantiationExpressionType, symbol) - c.setStructuredTypeMembers(result, resolved.members, callSignatures, constructSignatures, resolved.indexInfos) + c.setStructuredTypeMembers(result, resolved.members, callSignatures, constructSignatures, resolved.getIndexInfos()) result.AsInstantiationExpressionType().node = node return result } @@ -16159,7 +16160,7 @@ func (c *Checker) cloneTypeAsModuleType(symbol *ast.Symbol, moduleType *Type, re links.target = symbol links.originatingImport = referenceParent resolvedModuleType := c.resolveStructuredTypeMembers(moduleType) - c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.members, nil, nil, resolvedModuleType.indexInfos) + c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.members, nil, nil, resolvedModuleType.getIndexInfos()) return result } @@ -19423,10 +19424,13 @@ func (c *Checker) getSignaturesOfStructuredType(t *Type, kind SignatureKind) []* return nil } resolved := c.resolveStructuredTypeMembers(t) + if resolved.extra == nil { + return nil + } if kind == SignatureKindCall { - return resolved.signatures[:resolved.callSignatureCount] + return resolved.extra.signatures[:resolved.extra.callSignatureCount] } - return resolved.signatures[resolved.callSignatureCount:] + return resolved.extra.signatures[resolved.extra.callSignatureCount:] } func (c *Checker) getIndexInfosOfType(t *Type) []*IndexInfo { @@ -19435,7 +19439,7 @@ func (c *Checker) getIndexInfosOfType(t *Type) []*IndexInfo { func (c *Checker) getIndexInfosOfStructuredType(t *Type) []*IndexInfo { if t.flags&TypeFlagsStructuredType != 0 { - return c.resolveStructuredTypeMembers(t).indexInfos + return c.resolveStructuredTypeMembers(t).getIndexInfos() } return nil } @@ -19808,7 +19812,7 @@ func (c *Checker) getSingleCallOrConstructSignature(t *Type) *Signature { func (c *Checker) getSingleSignature(t *Type, kind SignatureKind, allowMembers bool) *Signature { if t.flags&TypeFlagsObject != 0 { resolved := c.resolveStructuredTypeMembers(t) - if allowMembers || len(resolved.properties) == 0 && len(resolved.indexInfos) == 0 { + if allowMembers || len(resolved.properties) == 0 && len(resolved.getIndexInfos()) == 0 { if kind == SignatureKindCall && len(resolved.CallSignatures()) == 1 && len(resolved.ConstructSignatures()) == 0 { return resolved.CallSignatures()[0] } @@ -21161,14 +21165,20 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { indexInfos = append(indexInfos, c.enumNumberIndexInfo) } } - d.indexInfos = indexInfos + if indexInfos != nil || d.extra != nil { + d.ensureExtra(c).indexInfos = indexInfos + } // We resolve the members before computing the signatures because a signature may use // typeof with a qualified name expression that circularly references the type we are // in the process of resolving (see issue #6072). The temporarily empty signature list // will never be observed because a qualified name can't reference signatures. if symbol.Flags()&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0 { - d.signatures = c.getSignaturesOfSymbol(symbol) - d.callSignatureCount = len(d.signatures) + signatures := c.getSignaturesOfSymbol(symbol) + if signatures != nil || d.extra != nil { + extra := d.ensureExtra(c) + extra.signatures = signatures + extra.callSignatureCount = len(signatures) + } } // And likewise for construct signatures for classes if symbol.Flags()&ast.SymbolFlagsClass != 0 { @@ -21177,7 +21187,10 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { if len(constructSignatures) == 0 { constructSignatures = c.getDefaultConstructSignatures(classType) } - d.signatures = append(d.signatures, constructSignatures...) + if len(constructSignatures) != 0 { + extra := d.ensureExtra(c) + extra.signatures = append(extra.signatures, constructSignatures...) + } } } @@ -25736,22 +25749,30 @@ func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, cal data := t.AsStructuredType() data.members = members data.properties = c.getNamedMembers(members, t.symbol) + c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos) +} + +func (c *Checker) setStructuredTypeSignatures(data *StructuredType, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { + if data.extra == nil && callSignatures == nil && constructSignatures == nil && indexInfos == nil { + return + } + extra := data.ensureExtra(c) if len(callSignatures) != 0 { if len(constructSignatures) != 0 { - data.signatures = core.Concatenate(callSignatures, constructSignatures) + extra.signatures = core.Concatenate(callSignatures, constructSignatures) } else { - data.signatures = slices.Clip(callSignatures) + extra.signatures = slices.Clip(callSignatures) } - data.callSignatureCount = len(callSignatures) + extra.callSignatureCount = len(callSignatures) } else { if len(constructSignatures) != 0 { - data.signatures = slices.Clip(constructSignatures) + extra.signatures = slices.Clip(constructSignatures) } else { - data.signatures = nil + extra.signatures = nil } - data.callSignatureCount = 0 + extra.callSignatureCount = 0 } - data.indexInfos = slices.Clip(indexInfos) + extra.indexInfos = slices.Clip(indexInfos) } func (c *Checker) newTypeParameter(symbol *ast.Symbol) *Type { @@ -27081,7 +27102,7 @@ func (c *Checker) IsEmptyAnonymousObjectType(t *Type) bool { } func (c *Checker) isEmptyResolvedType(t *StructuredType) bool { - return t.AsType() != c.anyFunctionType && len(t.properties) == 0 && len(t.signatures) == 0 && len(t.indexInfos) == 0 + return t.AsType() != c.anyFunctionType && len(t.properties) == 0 && len(t.getSignatures()) == 0 && len(t.getIndexInfos()) == 0 } func (c *Checker) isEmptyObjectType(t *Type) bool { @@ -28465,7 +28486,7 @@ func (c *Checker) containsUndefinedType(t *Type) bool { } func (c *Checker) typeHasCallOrConstructSignatures(t *Type) bool { - return t.flags&TypeFlagsStructuredType != 0 && len(c.resolveStructuredTypeMembers(t).signatures) != 0 + return t.flags&TypeFlagsStructuredType != 0 && len(c.resolveStructuredTypeMembers(t).getSignatures()) != 0 } func (c *Checker) getNormalizedType(t *Type, writing bool) *Type { @@ -28792,7 +28813,7 @@ func (c *Checker) getRegularTypeOfObjectLiteral(t *Type) *Type { } resolved := c.resolveStructuredTypeMembers(t) members := c.transformTypeOfMembers(t, c.getRegularTypeOfObjectLiteral) - regular := c.newAnonymousType(t.symbol, members, resolved.CallSignatures(), resolved.ConstructSignatures(), resolved.indexInfos) + regular := c.newAnonymousType(t.symbol, members, resolved.CallSignatures(), resolved.ConstructSignatures(), resolved.getIndexInfos()) regular.flags = resolved.flags regular.objectFlags |= resolved.objectFlags & ^ObjectFlagsFreshLiteral c.cachedTypes[key] = regular @@ -31789,7 +31810,7 @@ func (c *Checker) isFunctionObjectType(t *Type) bool { // We do a quick check for a "bind" property before performing the more expensive subtype // check. This gives us a quicker out in the common case where an object type is not a function. resolved := c.resolveStructuredTypeMembers(t) - return len(resolved.signatures) != 0 || resolved.members["bind"] != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) + return len(resolved.getSignatures()) != 0 || resolved.members["bind"] != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) } func (c *Checker) getTypeWithFacts(t *Type, include TypeFacts) *Type { diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index c7c545fc0d89a..036a06a843394 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -566,19 +566,19 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S if len(t.ConstructSignatures()) == 0 { return t.AsType() } - if t.objectTypeWithoutAbstractConstructSignatures != nil { - return t.objectTypeWithoutAbstractConstructSignatures + if t.extra.objectTypeWithoutAbstractConstructSignatures != nil { + return t.extra.objectTypeWithoutAbstractConstructSignatures } constructSignatures := core.Filter(t.ConstructSignatures(), func(signature *Signature) bool { return signature.flags&SignatureFlagsAbstract == 0 }) if len(constructSignatures) == len(t.ConstructSignatures()) { - t.objectTypeWithoutAbstractConstructSignatures = t.AsType() + t.extra.objectTypeWithoutAbstractConstructSignatures = t.AsType() return t.AsType() } - typeCopy := b.ch.newAnonymousType(t.symbol, t.members, t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.indexInfos) - t.objectTypeWithoutAbstractConstructSignatures = typeCopy - typeCopy.AsStructuredType().objectTypeWithoutAbstractConstructSignatures = typeCopy + typeCopy := b.ch.newAnonymousType(t.symbol, t.members, t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos()) + t.extra.objectTypeWithoutAbstractConstructSignatures = typeCopy + typeCopy.AsStructuredType().ensureExtra(b.ch).objectTypeWithoutAbstractConstructSignatures = typeCopy return typeCopy } @@ -2750,7 +2750,7 @@ func (b *NodeBuilderImpl) createTypeNodesFromResolvedType(resolvedType *Structur } typeElements = append(typeElements, b.signatureToSignatureDeclarationHelper(signature, ast.KindConstructSignature, nil)) } - for _, info := range resolvedType.indexInfos { + for _, info := range resolvedType.getIndexInfos() { var typeNode *ast.TypeNode if resolvedType.objectFlags&ObjectFlagsReverseMapped != 0 && b.ctx.flags&nodebuilder.FlagsAllowAnonymousIdentifier != 0 { typeNode = b.createElidedInformationPlaceholder() @@ -2807,7 +2807,7 @@ func (b *NodeBuilderImpl) createTypeNodeFromObjectType(t *Type) *ast.TypeNode { resolved := b.ch.resolveStructuredTypeMembers(t) callSigs := resolved.CallSignatures() ctorSigs := resolved.ConstructSignatures() - if len(resolved.properties) == 0 && len(resolved.indexInfos) == 0 { + if len(resolved.properties) == 0 && len(resolved.getIndexInfos()) == 0 { if len(callSigs) == 0 && len(ctorSigs) == 0 { b.ctx.approximateLength += 2 result := b.f.NewTypeLiteralNode(b.f.NewNodeList([]*ast.Node{})) @@ -2836,7 +2836,7 @@ func (b *NodeBuilderImpl) createTypeNodeFromObjectType(t *Type) *ast.TypeNode { return b.ch.getOrCreateTypeFromSignature(s) }) // count the number of type elements excluding abstract constructors - typeElementCount := len(callSigs) + (len(ctorSigs) - len(abstractSignatures)) + len(resolved.indexInfos) + core.IfElse(b.ctx.flags&nodebuilder.FlagsWriteClassExpressionAsTypeLiteral != 0, core.CountWhere(resolved.properties, func(p *ast.Symbol) bool { + typeElementCount := len(callSigs) + (len(ctorSigs) - len(abstractSignatures)) + len(resolved.getIndexInfos()) + core.IfElse(b.ctx.flags&nodebuilder.FlagsWriteClassExpressionAsTypeLiteral != 0, core.CountWhere(resolved.properties, func(p *ast.Symbol) bool { return p.Flags()&ast.SymbolFlagsPrototype == 0 }), len(resolved.properties)) // don't include an empty object literal if there were no other static-side diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index a9bc484fef1d2..8bf177bfd9e3a 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -679,7 +679,7 @@ func (c *Checker) elaborateArrowFunction(node *ast.Node, source *Type, target *T func (c *Checker) isWeakType(t *Type) bool { if t.flags&TypeFlagsObject != 0 { resolved := c.resolveStructuredTypeMembers(t) - return len(resolved.signatures) == 0 && len(resolved.indexInfos) == 0 && len(resolved.properties) > 0 && core.Every(resolved.properties, func(p *ast.Symbol) bool { + return len(resolved.getSignatures()) == 0 && len(resolved.getIndexInfos()) == 0 && len(resolved.properties) > 0 && core.Every(resolved.properties, func(p *ast.Symbol) bool { return p.Flags()&ast.SymbolFlagsOptional != 0 }) } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 4809ceed97cbb..e0692bdf73c75 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1096,8 +1096,12 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t } type StructuredType struct { ConstrainedType - members ast.SymbolTable - properties []*ast.Symbol + members ast.SymbolTable + properties []*ast.Symbol + extra *structuredTypeExtra +} + +type structuredTypeExtra struct { signatures []*Signature // Signatures (call + construct) callSignatureCount int // Count of call signatures indexInfos []*IndexInfo @@ -1108,11 +1112,38 @@ type StructuredType struct { func (t *StructuredType) AsStructuredType() *StructuredType { return t } func (t *StructuredType) CallSignatures() []*Signature { - return slices.Clip(t.signatures[:t.callSignatureCount]) + if t.extra == nil { + return nil + } + return slices.Clip(t.extra.signatures[:t.extra.callSignatureCount]) } func (t *StructuredType) ConstructSignatures() []*Signature { - return slices.Clip(t.signatures[t.callSignatureCount:]) + if t.extra == nil { + return nil + } + return slices.Clip(t.extra.signatures[t.extra.callSignatureCount:]) +} + +func (t *StructuredType) getSignatures() []*Signature { + if t.extra == nil { + return nil + } + return t.extra.signatures +} + +func (t *StructuredType) getIndexInfos() []*IndexInfo { + if t.extra == nil { + return nil + } + return t.extra.indexInfos +} + +func (t *StructuredType) ensureExtra(c *Checker) *structuredTypeExtra { + if t.extra == nil { + t.extra = c.structuredTypeExtraArena.New() + } + return t.extra } func (t *StructuredType) Properties() []*ast.Symbol { diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 286f5c6dc8368..d08d1803e2035 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -391,3 +391,53 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) { assert.Equal(t, links.getWriteType(), types[0]) assert.Equal(t, links.getContainingType(), types[2]) } + +func TestCompactStructuredTypeLayout(t *testing.T) { + t.Parallel() + assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0))) +} + +func TestStructuredTypeExtra(t *testing.T) { + t.Parallel() + c, types := newCompositeTestChecker() + data := c.newAnonymousType(nil, nil, nil, nil, nil).AsStructuredType() + assert.Assert(t, data.extra == nil) + assert.Assert(t, data.CallSignatures() == nil) + assert.Assert(t, data.ConstructSignatures() == nil) + assert.Assert(t, data.getIndexInfos() == nil) + c.setStructuredTypeSignatures(data, nil, nil, nil) + assert.Assert(t, data.extra == nil) + + call := &Signature{} + construct := &Signature{} + info := &IndexInfo{keyType: types[0]} + c.setStructuredTypeSignatures(data, []*Signature{call}, []*Signature{construct}, []*IndexInfo{info}) + assert.Equal(t, data.CallSignatures()[0], call) + assert.Equal(t, data.ConstructSignatures()[0], construct) + assert.Equal(t, cap(data.CallSignatures()), 1) + assert.Equal(t, cap(data.ConstructSignatures()), 1) + assert.Equal(t, data.getIndexInfos()[0], info) + extra := data.extra + extra.objectTypeWithoutAbstractConstructSignatures = types[2] + for range 1024 { + other := c.newAnonymousType(nil, nil, []*Signature{call}, nil, nil) + assert.Assert(t, other.AsStructuredType().extra != extra) + } + runtime.GC() + assert.Equal(t, data.CallSignatures()[0], call) + assert.Equal(t, data.ConstructSignatures()[0], construct) + assert.Equal(t, data.getIndexInfos()[0], info) + assert.Equal(t, data.extra.objectTypeWithoutAbstractConstructSignatures, types[2]) + c.setStructuredTypeSignatures(data, nil, []*Signature{construct}, nil) + assert.Equal(t, len(data.CallSignatures()), 0) + assert.Equal(t, data.ConstructSignatures()[0], construct) + assert.Assert(t, data.getIndexInfos() == nil) + c.setStructuredTypeSignatures(data, nil, nil, nil) + assert.Assert(t, data.CallSignatures() == nil) + assert.Assert(t, data.ConstructSignatures() == nil) + assert.Equal(t, data.extra, extra) + assert.Equal(t, extra.objectTypeWithoutAbstractConstructSignatures, types[2]) + c.setStructuredTypeSignatures(data, nil, nil, []*IndexInfo{}) + assert.Assert(t, data.getIndexInfos() != nil) + runtime.KeepAlive(c) +} From 66a2c76ba68aa4805952a3b5685512c75eda4a83 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:20:41 -0700 Subject: [PATCH 3/3] Find structured type extra storage through the type's checker Every type already records its checker, so callers no longer need to thread one through just to reach the extra-storage arena. --- tsc/internal/checker/checker.go | 8 ++++---- tsc/internal/checker/nodebuilderimpl.go | 2 +- tsc/internal/checker/types.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index ffa9629091a35..144c82f02c93a 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -21166,7 +21166,7 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { } } if indexInfos != nil || d.extra != nil { - d.ensureExtra(c).indexInfos = indexInfos + d.ensureExtra().indexInfos = indexInfos } // We resolve the members before computing the signatures because a signature may use // typeof with a qualified name expression that circularly references the type we are @@ -21175,7 +21175,7 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { if symbol.Flags()&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0 { signatures := c.getSignaturesOfSymbol(symbol) if signatures != nil || d.extra != nil { - extra := d.ensureExtra(c) + extra := d.ensureExtra() extra.signatures = signatures extra.callSignatureCount = len(signatures) } @@ -21188,7 +21188,7 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { constructSignatures = c.getDefaultConstructSignatures(classType) } if len(constructSignatures) != 0 { - extra := d.ensureExtra(c) + extra := d.ensureExtra() extra.signatures = append(extra.signatures, constructSignatures...) } } @@ -25756,7 +25756,7 @@ func (c *Checker) setStructuredTypeSignatures(data *StructuredType, callSignatur if data.extra == nil && callSignatures == nil && constructSignatures == nil && indexInfos == nil { return } - extra := data.ensureExtra(c) + extra := data.ensureExtra() if len(callSignatures) != 0 { if len(constructSignatures) != 0 { extra.signatures = core.Concatenate(callSignatures, constructSignatures) diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 036a06a843394..8e4b0c63f4d2a 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -578,7 +578,7 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S } typeCopy := b.ch.newAnonymousType(t.symbol, t.members, t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos()) t.extra.objectTypeWithoutAbstractConstructSignatures = typeCopy - typeCopy.AsStructuredType().ensureExtra(b.ch).objectTypeWithoutAbstractConstructSignatures = typeCopy + typeCopy.AsStructuredType().ensureExtra().objectTypeWithoutAbstractConstructSignatures = typeCopy return typeCopy } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index e0692bdf73c75..9266430f3a715 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1139,9 +1139,9 @@ func (t *StructuredType) getIndexInfos() []*IndexInfo { return t.extra.indexInfos } -func (t *StructuredType) ensureExtra(c *Checker) *structuredTypeExtra { +func (t *StructuredType) ensureExtra() *structuredTypeExtra { if t.extra == nil { - t.extra = c.structuredTypeExtraArena.New() + t.extra = t.checker.structuredTypeExtraArena.New() } return t.extra }