Skip to content

Commit 1f6c99a

Browse files
committed
Index structured type members without redundant symbol maps
Lazy member resolution removed the member maps of instantiated classes and interfaces, but anonymous, mapped, and other resolved object types still keep a name-to-symbol map beside their ordered property list. When every member is a named property stored under its own name, that map duplicates the property list, and its symbol pointers and string headers add to the live heap and to what the collector must scan. Use the property list as the source of truth for those types. Small types are searched directly, and larger ones get an index of pointer-free slots. Module exports, reserved members, and tables with other keys keep their maps.
1 parent 8a62bbe commit 1f6c99a

2 files changed

Lines changed: 101 additions & 11 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"iter"
88
"maps"
99
"math"
10+
"math/bits"
1011
"slices"
1112
"strconv"
1213
"strings"
@@ -692,6 +693,8 @@ type Checker struct {
692693
outerTypeParameterListArena core.Arena[[]*Type]
693694
valueSymbolExtraArena core.Arena[valueSymbolExtra]
694695
structuredTypeExtraArena core.Arena[structuredTypeExtra]
696+
memberIndexArena core.Arena[memberIndex]
697+
memberIndexSlotsArena core.Arena[uint32]
695698
lazyStateArena core.Arena[LazyState]
696699
memberLayoutArena core.Arena[MemberLayout]
697700
layoutMemberInfos map[string]MemberInfo
@@ -4592,8 +4595,8 @@ func (c *Checker) getTypeWithoutSignatures(t *Type) *Type {
45924595
if len(resolved.getSignatures()) != 0 {
45934596
result := c.newObjectType(ObjectFlagsAnonymous, t.symbol)
45944597
result.objectFlags |= ObjectFlagsMembersResolved
4595-
result.AsObjectType().members = resolved.getMembers()
45964598
result.AsObjectType().properties = resolved.getProperties()
4599+
result.AsObjectType().members = resolved.members
45974600
return result
45984601
}
45994602
case t.flags&TypeFlagsIntersection != 0:
@@ -25851,11 +25854,58 @@ func (c *Checker) cloneTypeReference(source *Type) *Type {
2585125854
func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) {
2585225855
t.objectFlags |= ObjectFlagsMembersResolved
2585325856
data := t.AsStructuredType()
25854-
data.members = members
25857+
// Filtering named members may query this type's members before they're set.
25858+
data.members = nil
25859+
if members != nil {
25860+
data.members = c.memberIndexArena.New()
25861+
data.members.table = members
25862+
}
2585525863
data.properties = c.getNamedMembers(members, t.symbol)
25864+
// Module exports can change after they're resolved, so keep their table.
25865+
if data.members != nil && (t.symbol == nil || t.symbol.Flags()&ast.SymbolFlagsValueModule == 0) && symbolTableMatchesProperties(members, data.properties) {
25866+
data.members = c.newPropertyIndex(data.properties, data.members)
25867+
}
2585625868
c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos)
2585725869
}
2585825870

25871+
// Reports whether every member is one of the properties, stored under its own
25872+
// name. Checking the table's pairs, rather than looking up each property by
25873+
// name, also rejects a symbol stored under a second key.
25874+
func symbolTableMatchesProperties(members ast.SymbolTable, properties []*ast.Symbol) bool {
25875+
if len(members) != len(properties) {
25876+
return false
25877+
}
25878+
for name, symbol := range members {
25879+
if symbol.Name() != name {
25880+
return false
25881+
}
25882+
}
25883+
return true
25884+
}
25885+
25886+
// Indexes properties by name in place of their symbol table. Small property
25887+
// lists are searched directly and need no index.
25888+
func (c *Checker) newPropertyIndex(properties []*ast.Symbol, index *memberIndex) *memberIndex {
25889+
if len(properties) <= 8 {
25890+
return nil
25891+
}
25892+
if uint64(len(properties)) >= uint64(^uint32(0)) {
25893+
panic("Member index exhausted")
25894+
}
25895+
size := 1 << bits.Len(uint(len(properties))*2-1)
25896+
index.table = nil
25897+
index.slots = c.memberIndexSlotsArena.NewSlice(size)
25898+
mask := uint64(size - 1)
25899+
for i, symbol := range properties {
25900+
slot := xxh3.HashString(symbol.Name()) & mask
25901+
for index.slots[slot] != 0 {
25902+
slot = (slot + 1) & mask
25903+
}
25904+
index.slots[slot] = uint32(i + 1)
25905+
}
25906+
return index
25907+
}
25908+
2585925909
func (c *Checker) setStructuredTypeSignatures(data *StructuredType, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) {
2586025910
if data.extra == nil && callSignatures == nil && constructSignatures == nil && indexInfos == nil {
2586125911
return

‎tsc/internal/checker/types.go‎

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/microsoft/TypeScript/tsc/internal/collections"
1111
"github.com/microsoft/TypeScript/tsc/internal/core"
1212
"github.com/microsoft/TypeScript/tsc/internal/evaluator"
13+
"github.com/zeebo/xxh3"
1314
)
1415

1516
//go:generate npx hereby generate:checker
@@ -1114,7 +1115,7 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t }
11141115

11151116
type StructuredType struct {
11161117
ConstrainedType
1117-
members ast.SymbolTable
1118+
members *memberIndex
11181119
properties []*ast.Symbol
11191120
extra *structuredTypeExtra
11201121
}
@@ -1145,10 +1146,15 @@ func (t *StructuredType) ConstructSignatures() []*Signature {
11451146

11461147
func (t *StructuredType) getMember(name string) *ast.Symbol {
11471148
if t.members != nil {
1148-
return t.members[name]
1149+
return t.members.get(name, t.properties)
11491150
}
11501151
lazy := t.getLazyState()
11511152
if lazy == nil {
1153+
for _, symbol := range t.properties {
1154+
if symbol.Name() == name {
1155+
return symbol
1156+
}
1157+
}
11521158
return nil
11531159
}
11541160
memberInfo := lazy.layout.memberInfoMap[name]
@@ -1159,15 +1165,49 @@ func (t *StructuredType) getMember(name string) *ast.Symbol {
11591165
}
11601166

11611167
func (t *StructuredType) getMembers() ast.SymbolTable {
1162-
if t.members == nil {
1163-
if properties := t.getProperties(); len(properties) != 0 {
1164-
t.members = make(ast.SymbolTable, len(properties))
1165-
for _, member := range properties {
1166-
t.members[member.Name()] = member
1167-
}
1168+
if t.members != nil && t.members.table != nil {
1169+
return t.members.table
1170+
}
1171+
properties := t.getProperties()
1172+
if len(properties) == 0 {
1173+
return nil
1174+
}
1175+
members := make(ast.SymbolTable, len(properties))
1176+
for _, member := range properties {
1177+
members[member.Name()] = member
1178+
}
1179+
if t.getLazyState() != nil {
1180+
t.members = t.checker.memberIndexArena.New()
1181+
t.members.table = members
1182+
}
1183+
return members
1184+
}
1185+
1186+
// A memberIndex finds a structured type's members by name. When every member
1187+
// is a named property stored under its own name, the property list already
1188+
// holds all of them, so larger types index it with pointer-free slots and
1189+
// smaller ones need no index at all. Other types keep their symbol table.
1190+
type memberIndex struct {
1191+
table ast.SymbolTable
1192+
slots []uint32
1193+
}
1194+
1195+
func (m *memberIndex) get(name string, properties []*ast.Symbol) *ast.Symbol {
1196+
if m.table != nil {
1197+
return m.table[name]
1198+
}
1199+
mask := uint64(len(m.slots) - 1)
1200+
slot := xxh3.HashString(name) & mask
1201+
for {
1202+
index := m.slots[slot]
1203+
if index == 0 {
1204+
return nil
1205+
}
1206+
if symbol := properties[index-1]; symbol.Name() == name {
1207+
return symbol
11681208
}
1209+
slot = (slot + 1) & mask
11691210
}
1170-
return t.members
11711211
}
11721212

11731213
func (t *StructuredType) getPropertyAt(index int) *ast.Symbol {

0 commit comments

Comments
 (0)