From 1f6c99a7e921bbe066b80178fb3e4fa565f749ab Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:19:13 -0700 Subject: [PATCH] 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. --- tsc/internal/checker/checker.go | 54 ++++++++++++++++++++++++++++-- tsc/internal/checker/types.go | 58 ++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 148cf5fde47dd..19c9876826949 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -7,6 +7,7 @@ import ( "iter" "maps" "math" + "math/bits" "slices" "strconv" "strings" @@ -692,6 +693,8 @@ type Checker struct { outerTypeParameterListArena core.Arena[[]*Type] valueSymbolExtraArena core.Arena[valueSymbolExtra] structuredTypeExtraArena core.Arena[structuredTypeExtra] + memberIndexArena core.Arena[memberIndex] + memberIndexSlotsArena core.Arena[uint32] lazyStateArena core.Arena[LazyState] memberLayoutArena core.Arena[MemberLayout] layoutMemberInfos map[string]MemberInfo @@ -4592,8 +4595,8 @@ func (c *Checker) getTypeWithoutSignatures(t *Type) *Type { if len(resolved.getSignatures()) != 0 { result := c.newObjectType(ObjectFlagsAnonymous, t.symbol) result.objectFlags |= ObjectFlagsMembersResolved - result.AsObjectType().members = resolved.getMembers() result.AsObjectType().properties = resolved.getProperties() + result.AsObjectType().members = resolved.members return result } case t.flags&TypeFlagsIntersection != 0: @@ -25851,11 +25854,58 @@ func (c *Checker) cloneTypeReference(source *Type) *Type { func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { t.objectFlags |= ObjectFlagsMembersResolved data := t.AsStructuredType() - data.members = members + // Filtering named members may query this type's members before they're set. + data.members = nil + if members != nil { + data.members = c.memberIndexArena.New() + data.members.table = members + } data.properties = c.getNamedMembers(members, t.symbol) + // Module exports can change after they're resolved, so keep their table. + if data.members != nil && (t.symbol == nil || t.symbol.Flags()&ast.SymbolFlagsValueModule == 0) && symbolTableMatchesProperties(members, data.properties) { + data.members = c.newPropertyIndex(data.properties, data.members) + } c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos) } +// Reports whether every member is one of the properties, stored under its own +// name. Checking the table's pairs, rather than looking up each property by +// name, also rejects a symbol stored under a second key. +func symbolTableMatchesProperties(members ast.SymbolTable, properties []*ast.Symbol) bool { + if len(members) != len(properties) { + return false + } + for name, symbol := range members { + if symbol.Name() != name { + return false + } + } + return true +} + +// Indexes properties by name in place of their symbol table. Small property +// lists are searched directly and need no index. +func (c *Checker) newPropertyIndex(properties []*ast.Symbol, index *memberIndex) *memberIndex { + if len(properties) <= 8 { + return nil + } + if uint64(len(properties)) >= uint64(^uint32(0)) { + panic("Member index exhausted") + } + size := 1 << bits.Len(uint(len(properties))*2-1) + index.table = nil + index.slots = c.memberIndexSlotsArena.NewSlice(size) + mask := uint64(size - 1) + for i, symbol := range properties { + slot := xxh3.HashString(symbol.Name()) & mask + for index.slots[slot] != 0 { + slot = (slot + 1) & mask + } + index.slots[slot] = uint32(i + 1) + } + return index +} + func (c *Checker) setStructuredTypeSignatures(data *StructuredType, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { if data.extra == nil && callSignatures == nil && constructSignatures == nil && indexInfos == nil { return diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index f119288a21744..a4669dc1a4cb6 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/evaluator" + "github.com/zeebo/xxh3" ) //go:generate npx hereby generate:checker @@ -1114,7 +1115,7 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t } type StructuredType struct { ConstrainedType - members ast.SymbolTable + members *memberIndex properties []*ast.Symbol extra *structuredTypeExtra } @@ -1145,10 +1146,15 @@ func (t *StructuredType) ConstructSignatures() []*Signature { func (t *StructuredType) getMember(name string) *ast.Symbol { if t.members != nil { - return t.members[name] + return t.members.get(name, t.properties) } lazy := t.getLazyState() if lazy == nil { + for _, symbol := range t.properties { + if symbol.Name() == name { + return symbol + } + } return nil } memberInfo := lazy.layout.memberInfoMap[name] @@ -1159,15 +1165,49 @@ func (t *StructuredType) getMember(name string) *ast.Symbol { } func (t *StructuredType) getMembers() ast.SymbolTable { - if t.members == nil { - if properties := t.getProperties(); len(properties) != 0 { - t.members = make(ast.SymbolTable, len(properties)) - for _, member := range properties { - t.members[member.Name()] = member - } + if t.members != nil && t.members.table != nil { + return t.members.table + } + properties := t.getProperties() + if len(properties) == 0 { + return nil + } + members := make(ast.SymbolTable, len(properties)) + for _, member := range properties { + members[member.Name()] = member + } + if t.getLazyState() != nil { + t.members = t.checker.memberIndexArena.New() + t.members.table = members + } + return members +} + +// A memberIndex finds a structured type's members by name. When every member +// is a named property stored under its own name, the property list already +// holds all of them, so larger types index it with pointer-free slots and +// smaller ones need no index at all. Other types keep their symbol table. +type memberIndex struct { + table ast.SymbolTable + slots []uint32 +} + +func (m *memberIndex) get(name string, properties []*ast.Symbol) *ast.Symbol { + if m.table != nil { + return m.table[name] + } + mask := uint64(len(m.slots) - 1) + slot := xxh3.HashString(name) & mask + for { + index := m.slots[slot] + if index == 0 { + return nil + } + if symbol := properties[index-1]; symbol.Name() == name { + return symbol } + slot = (slot + 1) & mask } - return t.members } func (t *StructuredType) getPropertyAt(index int) *ast.Symbol {