diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index e58e96048e94b..46c7e60194af8 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -690,6 +690,8 @@ type Checker struct { outerTypeParameterListArena core.Arena[[]*Type] valueSymbolExtraArena core.Arena[valueSymbolExtra] structuredTypeExtraArena core.Arena[structuredTypeExtra] + memberIndexArena core.Arena[memberIndex] + memberIndexSlotsArena core.Arena[uint32] freeTypeSets *temporaryTypeSet freePropertyCounts *temporaryPropertyCounts mergedSymbols map[*ast.Symbol]*ast.Symbol @@ -10951,7 +10953,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.getIndexInfos()) + c.setStructuredTypeMembers(result, resolved.getMembers(), callSignatures, constructSignatures, resolved.getIndexInfos()) result.AsInstantiationExpressionType().node = node return result } @@ -16163,7 +16165,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.getIndexInfos()) + c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.getMembers(), nil, nil, resolvedModuleType.getIndexInfos()) return result } @@ -19363,7 +19365,7 @@ func (c *Checker) getPropertyOfTypeEx(t *Type, name string, skipObjectFunctionPr switch { case t.flags&TypeFlagsObject != 0: resolved := c.resolveStructuredTypeMembers(t) - symbol := resolved.members[name] + symbol := resolved.getMember(name) if symbol != nil { if !includeTypeOnlyMembers && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 && c.moduleSymbolLinks.Get(t.symbol).typeOnlyExportStarMap[name] != nil { // If this is the type of a module, `resolved.members.get(name)` might have effectively skipped over @@ -19571,6 +19573,7 @@ func (c *Checker) resolveTypeReferenceMembers(t *Type) { func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters []*Type, typeArguments []*Type) { var mapper *TypeMapper var members ast.SymbolTable + var properties []*ast.Symbol var callSignatures []*Signature var constructSignatures []*Signature var indexInfos []*IndexInfo @@ -19584,7 +19587,14 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } else { instantiated = true mapper = newTypeMapper(typeParameters, typeArguments) - members = c.instantiateSymbolTable(resolved.declaredMembers, mapper) + if symbolTableKeysMatchNames(resolved.declaredMembers) { + properties = c.getNamedMembers(resolved.declaredMembers, t.symbol) + for i, symbol := range properties { + properties[i] = c.instantiateSymbol(symbol, mapper) + } + } else { + members = c.instantiateSymbolTable(resolved.declaredMembers, mapper) + } callSignatures = c.instantiateSignatures(resolved.declaredCallSignatures, mapper) constructSignatures = c.instantiateSignatures(resolved.declaredConstructSignatures, mapper) indexInfos = c.instantiateIndexInfos(resolved.declaredIndexInfos, mapper) @@ -19593,6 +19603,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters if len(baseTypes) != 0 { if !instantiated { members = maps.Clone(members) + } else if members == nil { + members = make(ast.SymbolTable, len(properties)) + for _, symbol := range properties { + members[symbol.Name()] = symbol + } } thisArgument := core.LastOrNil(typeArguments) for _, baseType := range baseTypes { @@ -19615,6 +19630,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } } c.setStructuredTypeMembers(t, members, callSignatures, constructSignatures, indexInfos) + if instantiated && len(baseTypes) == 0 && members == nil { + data := t.AsStructuredType() + data.properties = properties + data.members = c.newPropertyIndex(properties, nil) + } } func findIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo { @@ -21870,7 +21890,7 @@ func (c *Checker) includeMixinType(t *Type, types []*Type, mixinFlags []bool, in func (c *Checker) getPropertyOfObjectType(t *Type, name string) *ast.Symbol { if t.flags&TypeFlagsObject != 0 { resolved := c.resolveStructuredTypeMembers(t) - symbol := resolved.members[name] + symbol := resolved.getMember(name) if symbol != nil && c.symbolIsValue(symbol) { return symbol } @@ -25751,8 +25771,18 @@ 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 + // Named-member filtering may recursively query this type before it finishes. + data.members = nil + if members != nil { + data.members = c.memberIndexArena.New() + data.members.fallback = members + } data.properties = c.getNamedMembers(members, t.symbol) + if data.members != nil && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 { + data.members.fallback = members + } else { + data.members = c.newMemberIndex(members, data.properties, data.members) + } c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos) } @@ -31814,7 +31844,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.getSignatures()) != 0 || resolved.members["bind"] != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) + return len(resolved.getSignatures()) != 0 || resolved.getMember("bind") != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) } func (c *Checker) getTypeWithFacts(t *Type, include TypeFacts) *Type { @@ -32310,7 +32340,7 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy // member should more exactly be the kind of (declarationless) symbol we want. // (See #44364 and #45031 for relevant implementation PRs) if metaProp.KeywordToken == ast.KindImportKeyword && node.Text() == "meta" { - return c.getGlobalImportMetaExpressionType().AsObjectType().members["meta"] + return c.getGlobalImportMetaExpressionType().AsObjectType().getMember("meta") } // no other meta properties are valid syntax, thus no others should have symbols return nil diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 3d84ff3bd4094..d5f9f9a203864 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -576,7 +576,7 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S 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.getIndexInfos()) + typeCopy := b.ch.newAnonymousType(t.symbol, t.getMembers(), t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos()) t.extra.objectTypeWithoutAbstractConstructSignatures = typeCopy typeCopy.AsStructuredType().ensureExtra().objectTypeWithoutAbstractConstructSignatures = typeCopy return typeCopy diff --git a/tsc/internal/checker/services.go b/tsc/internal/checker/services.go index 90f3f2f901844..4926d142bbde5 100644 --- a/tsc/internal/checker/services.go +++ b/tsc/internal/checker/services.go @@ -154,10 +154,17 @@ func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb if reducedType.flags&TypeFlagsStructuredType == 0 { return } - for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members { - if c.isNamedMember(symbol, name) { - cb(symbol, name) + resolved := c.resolveStructuredTypeMembers(reducedType) + if resolved.members != nil && resolved.members.fallback != nil { + for name, symbol := range resolved.members.fallback { + if c.isNamedMember(symbol, name) { + cb(symbol, name) + } } + return + } + for _, symbol := range resolved.properties { + cb(symbol, symbol.Name()) } } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index a3cfe28c2e735..7fcac4815a7d4 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 } @@ -1168,6 +1169,107 @@ func (t *StructuredType) Properties() []*ast.Symbol { return t.properties } +type memberIndex struct { + slots []uint32 + fallback ast.SymbolTable +} + +// Property order is semantic. Keep it unchanged and index it with pointer-free +// slots instead of retaining a second map of names and symbol pointers. +func (c *Checker) newMemberIndex(members ast.SymbolTable, properties []*ast.Symbol, index *memberIndex) *memberIndex { + if len(members) != len(properties) || !symbolTableKeysMatchNames(members) { + if index == nil { + index = c.memberIndexArena.New() + } + index.fallback = members + return index + } + if index != nil { + index.fallback = nil + } + return c.newPropertyIndex(properties, index) +} + +// Reports whether every symbol is stored under its own name. Checking the +// table's pairs, rather than looking each property up by name, also rejects a +// symbol aliased under a second key, which a name-keyed index would drop. +func symbolTableKeysMatchNames(members ast.SymbolTable) bool { + for name, symbol := range members { + if symbol.Name() != name { + return false + } + } + return true +} + +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") + } + if index == nil { + index = c.memberIndexArena.New() + } + size := 1 << bits.Len(uint(len(properties))*2-1) + 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 (t *StructuredType) getMember(name string) *ast.Symbol { + if t.members == nil { + for _, symbol := range t.properties { + if symbol.Name() == name { + return symbol + } + } + return nil + } + if t.members.fallback != nil { + return t.members.fallback[name] + } + slots := t.members.slots + if len(slots) == 0 { + return nil + } + mask := uint64(len(slots) - 1) + slot := xxh3.HashString(name) & mask + for { + index := slots[slot] + if index == 0 { + return nil + } + symbol := t.properties[index-1] + if symbol.Name() == name { + return symbol + } + slot = (slot + 1) & mask + } +} + +func (t *StructuredType) getMembers() ast.SymbolTable { + if t.members != nil && t.members.fallback != nil { + return t.members.fallback + } + if len(t.properties) == 0 { + return nil + } + members := make(ast.SymbolTable, len(t.properties)) + for _, symbol := range t.properties { + members[symbol.Name()] = symbol + } + return members +} + // Except for tuple type references and reverse mapped types, all object types have an associated symbol. // Possible object type instances are listed in the following. diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..03897ea362bdd 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -507,6 +507,55 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) { assert.Equal(t, links.getContainingType(), types[2]) } +func TestStructuredMembersLookup(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + for _, count := range []int{0, 1, 8, 20, 300} { + members := make(ast.SymbolTable, count) + for i := range count { + name := strconv.Itoa(i) + members[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + typ := c.newAnonymousType(nil, members, nil, nil, nil) + resolved := typ.AsStructuredType() + assert.Equal(t, len(resolved.properties), count) + for name, symbol := range members { + assert.Equal(t, resolved.getMember(name), symbol) + } + assert.Assert(t, resolved.getMember("missing") == nil) + other := c.newAnonymousType(nil, resolved.getMembers(), nil, nil, nil) + for name, symbol := range members { + assert.Equal(t, other.AsStructuredType().getMember(name), symbol) + } + members["new"] = c.newSymbol(ast.SymbolFlagsProperty, "new") + assert.Assert(t, resolved.getMember("new") == nil) + members[ast.InternalSymbolNameCall] = c.newSymbol(ast.SymbolFlagsSignature, ast.InternalSymbolNameCall) + withReserved := c.newAnonymousType(nil, members, nil, nil, nil).AsStructuredType() + assert.Equal(t, withReserved.getMember(ast.InternalSymbolNameCall), members[ast.InternalSymbolNameCall]) + for range 300 { + c.newMemberIndex(members, withReserved.properties, nil) + } + runtime.GC() + for name, symbol := range members { + assert.Equal(t, withReserved.getMember(name), symbol) + } + } + renamed := c.newSymbol(ast.SymbolFlagsProperty, "original") + member := c.newAnonymousType(nil, ast.SymbolTable{"different": renamed}, nil, nil, nil).AsStructuredType() + assert.Equal(t, member.getMember("different"), renamed) + assert.Assert(t, member.getMember("original") == nil) + aliased := c.newSymbol(ast.SymbolFlagsProperty, "original") + aliasedMembers := ast.SymbolTable{"original": aliased, "different": aliased} + for i := range 10 { + name := "p" + strconv.Itoa(i) + aliasedMembers[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + aliasedType := c.newAnonymousType(nil, aliasedMembers, nil, nil, nil).AsStructuredType() + for name, symbol := range aliasedMembers { + assert.Equal(t, aliasedType.getMember(name), symbol) + } +} + func TestCompactStructuredTypeLayout(t *testing.T) { t.Parallel() assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0)))