From ad181684a4cedf5c546728c65d831815301ec6c9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:36:04 -0700 Subject: [PATCH 01/28] Resolve class and interface members through shared layouts Every instantiation of a generic class or interface eagerly built a full symbol table and instantiated every own and inherited member, even though most code only ever looks at a handful of them. On large projects this dominates checker memory: VS Code creates millions of instantiated symbols that are never used. The names, order, and flags of a class or interface's members do not depend on its type arguments, so compute them once per target as a layout that records where each member comes from, and share it across instantiations. Each instantiation then only holds a slot per member, filled on first access. There is a single path for resolving a member; full resolution is just resolving every slot, so property order and semantics are unchanged. Callers that only need the count, names, or flags of properties use the layout directly so they don't force resolution. --- tsc/internal/checker/checker.go | 389 ++++++++++++++++++++-- tsc/internal/checker/nodebuilder_hover.go | 2 +- tsc/internal/checker/nodebuilderimpl.go | 10 +- tsc/internal/checker/relater.go | 12 +- tsc/internal/checker/services.go | 2 +- tsc/internal/checker/types.go | 42 +++ 6 files changed, 416 insertions(+), 41 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index b03cebf741b22..18cfda165c218 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" @@ -4594,6 +4595,7 @@ func (c *Checker) getTypeWithoutSignatures(t *Type) *Type { result.objectFlags |= ObjectFlagsMembersResolved result.AsObjectType().members = resolved.members result.AsObjectType().properties = resolved.properties + result.AsObjectType().lazy = resolved.lazy return result } case t.flags&TypeFlagsIntersection != 0: @@ -10929,7 +10931,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.getMembers(), callSignatures, constructSignatures, resolved.indexInfos) result.AsInstantiationExpressionType().node = node return result } @@ -19267,9 +19269,32 @@ func (c *Checker) getPropertiesOfType(t *Type) []*ast.Symbol { return c.getPropertiesOfObjectType(t) } +// Returns symbols with the names and flags of the properties of a type, and, when the properties have not been +// resolved, the lazy members to resolve them from. When lazy is non-nil, the property at index i is +// c.getLazyMember(lazy, i); otherwise the returned symbols are the properties themselves. +func (c *Checker) getPropertyShapesOfType(t *Type) (shapes []*ast.Symbol, lazy *lazyMembers) { + t = c.getReducedApparentType(t) + if t.flags&TypeFlagsObject != 0 { + resolved := c.resolveStructuredTypeMembers(t) + if resolved.properties == nil && resolved.lazy != nil { + return resolved.lazy.layout.symbols[:resolved.lazy.layout.namedCount], resolved.lazy + } + return resolved.properties, nil + } + return c.getPropertiesOfType(t), nil +} + +func (c *Checker) getPropertyCountOfType(t *Type) int { + t = c.getReducedApparentType(t) + if t.flags&TypeFlagsObject != 0 { + return c.resolveStructuredTypeMembers(t).propertyCount() + } + return len(c.getPropertiesOfType(t)) +} + func (c *Checker) getPropertiesOfObjectType(t *Type) []*ast.Symbol { if t.flags&TypeFlagsObject != 0 { - return c.resolveStructuredTypeMembers(t).properties + return c.resolveStructuredTypeMembers(t).Properties() } return nil } @@ -19317,7 +19342,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 @@ -19521,51 +19546,356 @@ 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 callSignatures []*Signature var constructSignatures []*Signature var indexInfos []*IndexInfo var instantiated bool resolved := c.resolveDeclaredMembers(source) if slices.Equal(typeParameters, typeArguments) { - members = resolved.declaredMembers callSignatures = resolved.declaredCallSignatures constructSignatures = resolved.declaredConstructSignatures indexInfos = resolved.declaredIndexInfos } else { instantiated = true mapper = newTypeMapper(typeParameters, typeArguments) - members = c.instantiateSymbolTable(resolved.declaredMembers, mapper) callSignatures = c.instantiateSignatures(resolved.declaredCallSignatures, mapper) constructSignatures = c.instantiateSignatures(resolved.declaredConstructSignatures, mapper) indexInfos = c.instantiateIndexInfos(resolved.declaredIndexInfos, mapper) } baseTypes := c.getBaseTypes(source) + var instantiatedBaseTypes []*Type if len(baseTypes) != 0 { - if !instantiated { - members = maps.Clone(members) - } + instantiatedBaseTypes = make([]*Type, len(baseTypes)) thisArgument := core.LastOrNil(typeArguments) - for _, baseType := range baseTypes { + for i, baseType := range baseTypes { instantiatedBaseType := baseType if thisArgument != nil { instantiatedBaseType = c.getTypeWithThisArgument(c.instantiateType(baseType, mapper), thisArgument, false /*needsApparentType*/) } - members = c.addInheritedMembers(members, c.getPropertiesOfType(instantiatedBaseType)) - callSignatures = core.Concatenate(callSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindCall)) - constructSignatures = core.Concatenate(constructSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindConstruct)) - var inheritedIndexInfos []*IndexInfo - if instantiatedBaseType != c.anyType { - inheritedIndexInfos = c.getIndexInfosOfType(instantiatedBaseType) + instantiatedBaseTypes[i] = instantiatedBaseType + } + } + // Members are resolved on demand through a layout that is computed once and shared by all instantiations + // when possible. The layout must be computed before inherited signatures, which may circularly depend on it. + layout := c.getMemberLayout(source, instantiatedBaseTypes) + for _, instantiatedBaseType := range instantiatedBaseTypes { + callSignatures = core.Concatenate(callSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindCall)) + constructSignatures = core.Concatenate(constructSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindConstruct)) + var inheritedIndexInfos []*IndexInfo + if instantiatedBaseType != c.anyType { + inheritedIndexInfos = c.getIndexInfosOfType(instantiatedBaseType) + } else { + inheritedIndexInfos = []*IndexInfo{c.anyBaseTypeIndexInfo} + } + indexInfos = core.Concatenate(indexInfos, core.Filter(inheritedIndexInfos, func(info *IndexInfo) bool { + return findIndexInfo(indexInfos, info.keyType) == nil + })) + } + c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes, instantiated: instantiated}, callSignatures, constructSignatures, indexInfos) +} + +const ( + memberOriginOwn int32 = -1 + memberOriginFixed int32 = -2 + memberSlotByName int32 = -1 +) + +// A memberOrigin says where the member in a layout slot comes from: a declared member of the +// source type (instantiated with the instance mapper), a slot of the layout of a base type, the +// property with the same name in a base type without a layout, or a fixed symbol. +type memberOrigin struct { + base int32 + slot int32 +} + +// A memberLayout describes the members of a class or interface type and of all of its +// instantiations: their keys, their order, and where each one comes from. Named members come +// first, in property order. Member names and property order do not depend on type arguments +// when every base type has a shared layout, in which case the layout is computed once per +// class or interface and shared by every instantiation. +type memberLayout struct { + keys []string // Member keys, nil when every key is the symbol name + symbols []*ast.Symbol // Declared member, representative base member, or fixed member + origins []memberOrigin + index []uint32 // Open-addressed hash of 1-based slot numbers, nil for small layouts + namedCount int + shared bool +} + +func (l *memberLayout) key(i int) string { + if l.keys != nil { + return l.keys[i] + } + return l.symbols[i].Name() +} + +func (l *memberLayout) find(key string) int { + if l.index == nil { + for i := range l.symbols { + if l.key(i) == key { + return i + } + } + return -1 + } + mask := uint64(len(l.index) - 1) + slot := xxh3.HashString(key) & mask + for { + i := l.index[slot] + if i == 0 { + return -1 + } + if l.key(int(i-1)) == key { + return int(i - 1) + } + slot = (slot + 1) & mask + } +} + +// The members of a class or interface type or one of its instantiations. Each member is +// resolved on first use and cached in its slot, so full resolution is just resolving every slot. +type lazyMembers struct { + c *Checker + layout *memberLayout + mapper *TypeMapper // Instantiation mapper, nil for the declared type + bases []*Type // Base types, instantiated and with this-type arguments + slots []*ast.Symbol + instantiated bool +} + +func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { + d := source.AsInterfaceType() + if d.memberLayout != nil { + return d.memberLayout + } + layout := c.buildMemberLayout(source, bases) + if layout.shared { + d.memberLayout = layout + } + return layout +} + +func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { + type entry struct { + key string + symbol *ast.Symbol + origin memberOrigin + } + declared := source.AsInterfaceType().declaredMembers + entries := make([]entry, 0, len(declared)) + index := make(map[string]int, len(declared)) + for key, symbol := range declared { + index[key] = len(entries) + entries = append(entries, entry{key, symbol, memberOrigin{memberOriginOwn, 0}}) + } + // A layout computed while base types are still being resolved may be missing inherited members. + shared := source.AsInterfaceType().baseTypesResolved + var declaredBases []*Type + if shared { + declaredBases = c.getBaseTypes(source) + } + for k, base := range bases { + if base == c.anyType { + continue + } + var baseLayout *memberLayout + var baseSymbols []*ast.Symbol + if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { + if resolved := c.resolveStructuredTypeMembers(base); resolved.lazy != nil { + baseLayout = resolved.lazy.layout + } + } + count := 0 + byName := false + if baseLayout != nil { + shared = shared && baseLayout.shared + count = baseLayout.namedCount + } else { + // The properties of a base type without a layout have the same names in every instantiation + // when the declared base type references no type parameters, so they can be found by name. + byName = shared && k < len(declaredBases) && !c.couldContainTypeVariables(declaredBases[k]) + shared = byName + baseSymbols = c.getPropertiesOfType(base) + count = len(baseSymbols) + } + for i := range count { + var symbol *ast.Symbol + var origin memberOrigin + if baseLayout != nil { + symbol = baseLayout.symbols[i] + origin = memberOrigin{int32(k), int32(i)} } else { - inheritedIndexInfos = []*IndexInfo{c.anyBaseTypeIndexInfo} + symbol = baseSymbols[i] + origin = memberOrigin{memberOriginFixed, 0} + if byName { + origin = memberOrigin{int32(k), memberSlotByName} + } + } + if isStaticPrivateIdentifierProperty(symbol) { + continue + } + name := symbol.Name() + if j, ok := index[name]; !ok { + index[name] = len(entries) + entries = append(entries, entry{name, symbol, origin}) + } else if entries[j].symbol.Flags()&ast.SymbolFlagsValue == 0 { + entries[j] = entry{name, symbol, origin} } - indexInfos = core.Concatenate(indexInfos, core.Filter(inheritedIndexInfos, func(info *IndexInfo) bool { - return findIndexInfo(indexInfos, info.keyType) == nil - })) } } - c.setStructuredTypeMembers(t, members, callSignatures, constructSignatures, indexInfos) + // For classes and interfaces, explicitly declared members come ahead of inherited members (see getNamedMembers). + container := source.symbol + isClassOrInterface := container != nil && container.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 + rank := func(e entry) int { + switch { + case !c.isNamedMember(e.symbol, e.key): + return 2 + case isClassOrInterface && c.isDeclarationContainedBy(e.symbol, container): + return 0 + default: + return 1 + } + } + ranks := make(map[string]int, len(entries)) + namedCount := 0 + for _, e := range entries { + r := rank(e) + ranks[e.key] = r + if r != 2 { + namedCount++ + } + } + slices.SortFunc(entries, func(a, b entry) int { + ra, rb := ranks[a.key], ranks[b.key] + if ra != rb { + return ra - rb + } + if ra == 2 { + return 0 + } + return c.compareSymbols(a.symbol, b.symbol) + }) + layout := &memberLayout{ + symbols: make([]*ast.Symbol, len(entries)), + origins: make([]memberOrigin, len(entries)), + namedCount: namedCount, + shared: shared, + } + for i, e := range entries { + layout.symbols[i] = e.symbol + layout.origins[i] = e.origin + if e.key != e.symbol.Name() && layout.keys == nil { + layout.keys = make([]string, len(entries)) + for j := range i { + layout.keys[j] = entries[j].key + } + } + if layout.keys != nil { + layout.keys[i] = e.key + } + } + layout.buildIndex() + return layout +} + +func (l *memberLayout) buildIndex() { + if len(l.symbols) <= 8 { + return + } + size := 1 << bits.Len(uint(len(l.symbols))*2-1) + l.index = make([]uint32, size) + mask := uint64(size - 1) + for i := range l.symbols { + slot := xxh3.HashString(l.key(i)) & mask + for l.index[slot] != 0 { + slot = (slot + 1) & mask + } + l.index[slot] = uint32(i + 1) + } +} + +func (c *Checker) setLazyStructuredTypeMembers(t *Type, lazy *lazyMembers, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { + c.setStructuredTypeMembers(t, nil, callSignatures, constructSignatures, indexInfos) + data := t.AsStructuredType() + data.properties = nil + data.lazy = lazy +} + +func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { + if l.slots == nil { + l.slots = make([]*ast.Symbol, len(l.layout.symbols)) + } + if symbol := l.slots[i]; symbol != nil { + return symbol + } + var symbol *ast.Symbol + origin := l.layout.origins[i] + switch origin.base { + case memberOriginOwn: + symbol = l.layout.symbols[i] + if l.instantiated { + symbol = c.instantiateSymbol(symbol, l.mapper) + } + case memberOriginFixed: + symbol = l.layout.symbols[i] + default: + if origin.slot == memberSlotByName { + symbol = c.getBasePropertyByName(l.bases[origin.base], l.layout.key(i)) + break + } + resolved := c.resolveStructuredTypeMembers(l.bases[origin.base]) + debug.Assert(resolved.lazy != nil, "Base type must have lazy members") + symbol = c.getLazyMember(resolved.lazy, int(origin.slot)) + } + l.slots[i] = symbol + return symbol +} + +// Returns the property of a base type with the given name, as found in getPropertiesOfType. +func (c *Checker) getBasePropertyByName(base *Type, name string) *ast.Symbol { + base = c.getReducedApparentType(base) + var symbol *ast.Symbol + if base.flags&TypeFlagsUnionOrIntersection != 0 { + symbol = c.getPropertyOfUnionOrIntersectionType(base, name, base.flags&TypeFlagsIntersection != 0 /*skipObjectFunctionPropertyAugment*/) + } else if base.flags&TypeFlagsObject != 0 { + symbol = c.resolveStructuredTypeMembers(base).getMember(name) + } + debug.Assert(symbol != nil, "Base type must have property") + return symbol +} + +func (c *Checker) getLazyMemberByName(l *lazyMembers, name string) *ast.Symbol { + i := l.layout.find(name) + if i < 0 || l.instantiated && i >= l.layout.namedCount { + return nil + } + return c.getLazyMember(l, i) +} + +func (c *Checker) getLazyProperties(l *lazyMembers) []*ast.Symbol { + n := l.layout.namedCount + if n == 0 { + return nil + } + for i := range n { + c.getLazyMember(l, i) + } + return l.slots[:n:n] +} + +func (c *Checker) getLazyMemberTable(l *lazyMembers) ast.SymbolTable { + n := len(l.layout.symbols) + if l.instantiated { + n = l.layout.namedCount + } + if n == 0 { + return nil + } + members := make(ast.SymbolTable, n) + for i := range n { + members[l.layout.key(i)] = c.getLazyMember(l, i) + } + return members } func findIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo { @@ -19766,7 +20096,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 || resolved.propertyCount() == 0 && len(resolved.indexInfos) == 0 { if kind == SignatureKindCall && len(resolved.CallSignatures()) == 1 && len(resolved.ConstructSignatures()) == 0 { return resolved.CallSignatures()[0] } @@ -21113,7 +21443,7 @@ func (c *Checker) resolveAnonymousTypeMembers(t *Type) { if baseConstructorIndexInfo != nil { indexInfos = append(indexInfos, baseConstructorIndexInfo) } - if symbol.Flags()&ast.SymbolFlagsEnum != 0 && (c.getDeclaredTypeOfSymbol(symbol).flags&TypeFlagsEnum != 0 || core.Some(d.properties, func(prop *ast.Symbol) bool { + if symbol.Flags()&ast.SymbolFlagsEnum != 0 && (c.getDeclaredTypeOfSymbol(symbol).flags&TypeFlagsEnum != 0 || core.Some(d.Properties(), func(prop *ast.Symbol) bool { return c.getTypeOfSymbol(prop).flags&TypeFlagsNumberLike != 0 })) { indexInfos = append(indexInfos, c.enumNumberIndexInfo) @@ -21812,7 +22142,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 } @@ -22289,7 +22619,8 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { // Collect declaration counts for each property across all constituent types of the intersection. var counts collections.OrderedMap[string, int] for _, t := range t.Types() { - for _, prop := range c.getPropertiesOfType(t) { + shapes, _ := c.getPropertyShapesOfType(t) + for _, prop := range shapes { counts.Set(prop.Name(), counts.GetOrZero(prop.Name())+1) } } @@ -27018,7 +27349,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 && t.propertyCount() == 0 && len(t.signatures) == 0 && len(t.indexInfos) == 0 } func (c *Checker) isEmptyObjectType(t *Type) bool { @@ -27675,7 +28006,7 @@ func (c *Checker) getPropertyTypeForIndexType(originalObjectType *Type, objectTy c.addDiagnostic(createDiagnosticForNode(accessExpression, diagnostics.Property_0_does_not_exist_on_type_1, indexType.AsLiteralType().value, c.TypeToString(objectType))) return c.undefinedType } else if indexType.flags&(TypeFlagsNumber|TypeFlagsString) != 0 { - types := core.Map(objectType.AsStructuredType().properties, func(prop *ast.Symbol) *Type { + types := core.Map(objectType.AsStructuredType().Properties(), func(prop *ast.Symbol) *Type { return c.getTypeOfSymbol(prop) }) return c.getUnionType(append(types, c.undefinedType)) @@ -27902,7 +28233,7 @@ func (c *Checker) getPropertyNameFromIndex(indexType *Type, accessNode *ast.Node } func (c *Checker) isStringIndexSignatureOnlyTypeWorker(t *Type) bool { - return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && len(c.getPropertiesOfType(t)) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || + return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && c.getPropertyCountOfType(t) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || t.flags&TypeFlagsUnionOrIntersection != 0 && core.Every(t.Types(), c.isStringIndexSignatureOnlyType) } @@ -31698,7 +32029,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.signatures) != 0 || resolved.getMember("bind") != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) } func (c *Checker) getTypeWithFacts(t *Type, include TypeFacts) *Type { diff --git a/tsc/internal/checker/nodebuilder_hover.go b/tsc/internal/checker/nodebuilder_hover.go index b048c5d1618c6..859bc6d5caa19 100644 --- a/tsc/internal/checker/nodebuilder_hover.go +++ b/tsc/internal/checker/nodebuilder_hover.go @@ -263,7 +263,7 @@ func (b *NodeBuilderImpl) expandInterfaceDecl(symbol *ast.Symbol) *ast.Node { members = append(members, b.signatureToSignatureDeclarationHelper(sig, ast.KindCallSignature, nil)) } // Properties, filtering inherited - filteredProps := b.filterInheritedProperties(interfaceType, baseTypes, resolved.properties) + filteredProps := b.filterInheritedProperties(interfaceType, baseTypes, resolved.Properties()) members = b.serializePropertiesWithTruncation(filteredProps, members) // Heritage clauses diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index cd4643ac6f273..38123c9ef1388 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -576,7 +576,7 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S t.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) + typeCopy := b.ch.newAnonymousType(t.symbol, t.getMembers(), t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.indexInfos) t.objectTypeWithoutAbstractConstructSignatures = typeCopy typeCopy.AsStructuredType().objectTypeWithoutAbstractConstructSignatures = typeCopy return typeCopy @@ -2745,7 +2745,7 @@ func (b *NodeBuilderImpl) createTypeNodesFromResolvedType(resolvedType *Structur typeElements = slices.Concat(typeElements, b.indexInfoToObjectComputedNamesOrSignatureDeclaration(info, typeNode)) } - properties := resolvedType.properties + properties := resolvedType.Properties() if len(properties) == 0 { return b.f.NewNodeList(typeElements) } @@ -2794,7 +2794,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.indexInfos) == 0 { if len(callSigs) == 0 && len(ctorSigs) == 0 { b.ctx.approximateLength += 2 result := b.f.NewTypeLiteralNode(b.f.NewNodeList([]*ast.Node{})) @@ -2823,9 +2823,9 @@ 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.indexInfos) + 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)) + }), len(resolved.Properties())) // don't include an empty object literal if there were no other static-side // properties to write, i.e. `abstract class C { }` becomes `abstract new () => {}` // and not `(abstract new () => {}) & {}` diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index a9bc484fef1d2..46620a34bd36a 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -679,9 +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 p.Flags()&ast.SymbolFlagsOptional != 0 - }) + return len(resolved.signatures) == 0 && len(resolved.indexInfos) == 0 && resolved.propertyCount() > 0 && resolved.everyPropertyHasFlags(ast.SymbolFlagsOptional) } if t.flags&TypeFlagsSubstitution != 0 { return c.isWeakType(t.AsSubstitutionType().baseType) @@ -975,14 +973,18 @@ func (c *Checker) getUnmatchedProperties(source *Type, target *Type, requireOpti } func (c *Checker) getUnmatchedPropertiesWorker(source *Type, target *Type, requireOptionalProperties bool, matchDiscriminantProperties bool, propsOut *[]*ast.Symbol) *ast.Symbol { - properties := c.getPropertiesOfType(target) - for _, targetProp := range properties { + // Names and flags of properties are known without resolving them, so only resolve the ones we report or compare. + shapes, lazy := c.getPropertyShapesOfType(target) + for i, targetProp := range shapes { // TODO: remove this when we support static private identifier fields and find other solutions to get privateNamesAndStaticFields test to pass if isStaticPrivateIdentifierProperty(targetProp) { continue } if requireOptionalProperties || targetProp.Flags()&ast.SymbolFlagsOptional == 0 && targetProp.CheckFlags()&ast.CheckFlagsPartial == 0 { sourceProp := c.getPropertyOfType(source, targetProp.Name()) + if lazy != nil && (sourceProp == nil || matchDiscriminantProperties) { + targetProp = c.getLazyMember(lazy, i) + } if sourceProp == nil { if propsOut == nil { return targetProp diff --git a/tsc/internal/checker/services.go b/tsc/internal/checker/services.go index cdcb404ba271d..32bf1b4168ca7 100644 --- a/tsc/internal/checker/services.go +++ b/tsc/internal/checker/services.go @@ -154,7 +154,7 @@ func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb if reducedType.flags&TypeFlagsStructuredType == 0 { return } - for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members { + for name, symbol := range c.resolveStructuredTypeMembers(reducedType).getMembers() { if c.isNamedMember(symbol, name) { cb(symbol, name) } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 7e4748930262c..6b8d2e322723a 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -936,6 +936,7 @@ type StructuredType struct { ConstrainedType members ast.SymbolTable properties []*ast.Symbol + lazy *lazyMembers // Members resolved on demand, in which case members and properties are nil until needed signatures []*Signature // Signatures (call + construct) callSignatureCount int // Count of call signatures indexInfos []*IndexInfo @@ -954,9 +955,49 @@ func (t *StructuredType) ConstructSignatures() []*Signature { } func (t *StructuredType) Properties() []*ast.Symbol { + if t.properties == nil && t.lazy != nil { + t.properties = t.lazy.c.getLazyProperties(t.lazy) + } return t.properties } +// Returns the number of properties without resolving them. +func (t *StructuredType) propertyCount() int { + if t.properties == nil && t.lazy != nil { + return t.lazy.layout.namedCount + } + return len(t.properties) +} + +// Reports whether every property has the given symbol flags, without resolving the properties. Instantiation +// preserves symbol flags, so the layout's representative symbols answer this for every instantiation. +func (t *StructuredType) everyPropertyHasFlags(flags ast.SymbolFlags) bool { + properties := t.properties + if properties == nil && t.lazy != nil { + properties = t.lazy.layout.symbols[:t.lazy.layout.namedCount] + } + for _, p := range properties { + if p.Flags()&flags == 0 { + return false + } + } + return true +} + +func (t *StructuredType) getMember(name string) *ast.Symbol { + if t.lazy != nil { + return t.lazy.c.getLazyMemberByName(t.lazy, name) + } + return t.members[name] +} + +func (t *StructuredType) getMembers() ast.SymbolTable { + if t.members == nil && t.lazy != nil { + t.members = t.lazy.c.getLazyMemberTable(t.lazy) + } + return t.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. @@ -1024,6 +1065,7 @@ type InterfaceType struct { declaredCallSignatures []*Signature // Declared call signatures declaredConstructSignatures []*Signature // Declared construct signatures declaredIndexInfos []*IndexInfo // Declared index signatures + memberLayout *memberLayout // Member layout shared by all instantiations, if possible } func (t *InterfaceType) AsInterfaceType() *InterfaceType { return t } From c7b046b84b8618c7a88eef18e16f924712e82b0b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:49:02 -0700 Subject: [PATCH 02/28] Rebuild member layouts when a base instantiation fails Once a statement exceeds the instantiation count limit, every further instantiation yields the error type, including the base types of a class or interface instance whose members are resolved afterward. The shared layout of that class or interface still expects to find inherited members in those bases, so resolving such a member crashed. Without a shared layout, an error base type simply contributes no members, matching the behavior before shared layouts. --- tsc/internal/checker/checker.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 18cfda165c218..656a92c6041a8 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19666,7 +19666,12 @@ type lazyMembers struct { func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { d := source.AsInterfaceType() if d.memberLayout != nil { - return d.memberLayout + // A base type instantiation that hit an instantiation limit is the error type, which has + // none of the members the shared layout expects to find in it. + if !slices.Contains(bases, c.errorType) || slices.Contains(c.getBaseTypes(source), c.errorType) { + return d.memberLayout + } + return c.buildMemberLayout(source, bases) } layout := c.buildMemberLayout(source, bases) if layout.shared { From ecc74a8976efe6d8177d64a9d7c3bd11a838e514 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:02:30 -0700 Subject: [PATCH 03/28] Reuse scratch storage when building member layouts Building a member layout allocated a fresh entry slice, name map, and rank map every time, which made it one of the larger sources of allocation in the checker: about 10% of all bytes allocated when checking VS Code. Layouts are built often enough, and the scratch storage is discarded immediately, so keep it on the checker and reuse it instead. On VS Code, bytes allocated by layout building drop from 503MB to 86MB, total bytes allocated from 4.97GB to 4.41GB, and allocation count by about 2%. --- tsc/internal/checker/checker.go | 141 +++++++++++++++++--------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 656a92c6041a8..9482fe46b6099 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -597,6 +597,8 @@ type Checker struct { TotalInstantiationCount uint32 instantiationCount uint32 instantiationStack []*Type + layoutEntries []layoutEntry + layoutIndex map[string]int conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -19680,102 +19682,110 @@ func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { return layout } +type layoutEntry struct { + key string + symbol *ast.Symbol + origin memberOrigin + rank int +} + func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { - type entry struct { - key string - symbol *ast.Symbol - origin memberOrigin + type baseMembers struct { + layout *memberLayout + symbols []*ast.Symbol + byName bool } declared := source.AsInterfaceType().declaredMembers - entries := make([]entry, 0, len(declared)) - index := make(map[string]int, len(declared)) - for key, symbol := range declared { - index[key] = len(entries) - entries = append(entries, entry{key, symbol, memberOrigin{memberOriginOwn, 0}}) - } // A layout computed while base types are still being resolved may be missing inherited members. shared := source.AsInterfaceType().baseTypesResolved var declaredBases []*Type if shared { declaredBases = c.getBaseTypes(source) } + // Resolve base members before taking the scratch storage below, since this may build other layouts. + baseMembersList := make([]baseMembers, len(bases)) + capacity := len(declared) for k, base := range bases { if base == c.anyType { continue } - var baseLayout *memberLayout - var baseSymbols []*ast.Symbol if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { if resolved := c.resolveStructuredTypeMembers(base); resolved.lazy != nil { - baseLayout = resolved.lazy.layout - } - } - count := 0 - byName := false - if baseLayout != nil { - shared = shared && baseLayout.shared - count = baseLayout.namedCount - } else { - // The properties of a base type without a layout have the same names in every instantiation - // when the declared base type references no type parameters, so they can be found by name. - byName = shared && k < len(declaredBases) && !c.couldContainTypeVariables(declaredBases[k]) - shared = byName - baseSymbols = c.getPropertiesOfType(base) - count = len(baseSymbols) - } - for i := range count { - var symbol *ast.Symbol - var origin memberOrigin - if baseLayout != nil { - symbol = baseLayout.symbols[i] - origin = memberOrigin{int32(k), int32(i)} - } else { - symbol = baseSymbols[i] - origin = memberOrigin{memberOriginFixed, 0} - if byName { - origin = memberOrigin{int32(k), memberSlotByName} - } - } - if isStaticPrivateIdentifierProperty(symbol) { + baseMembersList[k].layout = resolved.lazy.layout + shared = shared && resolved.lazy.layout.shared + capacity += resolved.lazy.layout.namedCount continue } - name := symbol.Name() - if j, ok := index[name]; !ok { - index[name] = len(entries) - entries = append(entries, entry{name, symbol, origin}) - } else if entries[j].symbol.Flags()&ast.SymbolFlagsValue == 0 { - entries[j] = entry{name, symbol, origin} + } + // The properties of a base type without a layout have the same names in every instantiation + // when the declared base type references no type parameters, so they can be found by name. + byName := shared && k < len(declaredBases) && !c.couldContainTypeVariables(declaredBases[k]) + shared = byName + baseMembersList[k].byName = byName + baseMembersList[k].symbols = c.getPropertiesOfType(base) + capacity += len(baseMembersList[k].symbols) + } + entries, index := c.layoutEntries[:0], c.layoutIndex + c.layoutEntries, c.layoutIndex = nil, nil + if cap(entries) < capacity { + entries = make([]layoutEntry, 0, capacity) + } + if index == nil { + index = make(map[string]int, capacity) + } + add := func(name string, symbol *ast.Symbol, origin memberOrigin) { + if isStaticPrivateIdentifierProperty(symbol) { + return + } + if j, ok := index[name]; !ok { + index[name] = len(entries) + entries = append(entries, layoutEntry{key: name, symbol: symbol, origin: origin}) + } else if entries[j].symbol.Flags()&ast.SymbolFlagsValue == 0 { + entries[j] = layoutEntry{key: name, symbol: symbol, origin: origin} + } + } + for key, symbol := range declared { + index[key] = len(entries) + entries = append(entries, layoutEntry{key: key, symbol: symbol, origin: memberOrigin{memberOriginOwn, 0}}) + } + for k, base := range baseMembersList { + if base.layout != nil { + for i, symbol := range base.layout.symbols[:base.layout.namedCount] { + add(symbol.Name(), symbol, memberOrigin{int32(k), int32(i)}) } + continue + } + origin := memberOrigin{memberOriginFixed, 0} + if base.byName { + origin = memberOrigin{int32(k), memberSlotByName} + } + for _, symbol := range base.symbols { + add(symbol.Name(), symbol, origin) } } // For classes and interfaces, explicitly declared members come ahead of inherited members (see getNamedMembers). container := source.symbol isClassOrInterface := container != nil && container.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 - rank := func(e entry) int { + namedCount := 0 + for i := range entries { + e := &entries[i] switch { case !c.isNamedMember(e.symbol, e.key): - return 2 + e.rank = 2 case isClassOrInterface && c.isDeclarationContainedBy(e.symbol, container): - return 0 + e.rank = 0 default: - return 1 + e.rank = 1 } - } - ranks := make(map[string]int, len(entries)) - namedCount := 0 - for _, e := range entries { - r := rank(e) - ranks[e.key] = r - if r != 2 { + if e.rank != 2 { namedCount++ } } - slices.SortFunc(entries, func(a, b entry) int { - ra, rb := ranks[a.key], ranks[b.key] - if ra != rb { - return ra - rb + slices.SortFunc(entries, func(a, b layoutEntry) int { + if a.rank != b.rank { + return a.rank - b.rank } - if ra == 2 { + if a.rank == 2 { return 0 } return c.compareSymbols(a.symbol, b.symbol) @@ -19799,6 +19809,9 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { layout.keys[i] = e.key } } + clear(entries) + clear(index) + c.layoutEntries, c.layoutIndex = entries, index layout.buildIndex() return layout } From 095d945fa8608b8ce099f6861881b73114f385c8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:14:34 -0700 Subject: [PATCH 04/28] Make member layouts easier to follow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback was that the member layout code is hard to read. Member origins were encoded with negative sentinels in the base and slot fields, the sort ranks were bare numbers, and callers that wanted property names and flags without resolving members had to juggle a pair of return values whose meaning depended on each other. Give member origins an explicit kind, name the sort ranks, and replace the shapes-and-lazy-members pair with getPropertyShapesOfType and getPropertyOfTypeAt, which work the same for every type. The compact layout representation is kept: a map index and per-member keys cost about 4% more memory on VS Code, since every interface layout includes all of its inherited members. │ before │ after │ │ heap-B/op │ heap-B/op vs base │ vscode 3.539Gi ± ∞ 3.548Gi ± ∞ +0.25% (p=0.029 n=4) mui 2.164Gi ± ∞ 2.169Gi ± ∞ +0.23% (p=0.029 n=4) │ before │ after │ │ allocs/op │ allocs/op vs base │ vscode 36.24M ± ∞ 36.26M ± ∞ ~ (p=0.886 n=4) mui 32.13M ± ∞ 32.18M ± ∞ ~ (p=0.686 n=4) --- tsc/internal/checker/checker.go | 317 ++++++++++++++++---------------- tsc/internal/checker/relater.go | 14 +- tsc/internal/checker/types.go | 24 +-- 3 files changed, 172 insertions(+), 183 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 9482fe46b6099..c58d6686f9401 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19271,27 +19271,22 @@ func (c *Checker) getPropertiesOfType(t *Type) []*ast.Symbol { return c.getPropertiesOfObjectType(t) } -// Returns symbols with the names and flags of the properties of a type, and, when the properties have not been -// resolved, the lazy members to resolve them from. When lazy is non-nil, the property at index i is -// c.getLazyMember(lazy, i); otherwise the returned symbols are the properties themselves. -func (c *Checker) getPropertyShapesOfType(t *Type) (shapes []*ast.Symbol, lazy *lazyMembers) { +// Like getPropertiesOfType, but returns symbols with only the right names and flags, without resolving lazy members. +func (c *Checker) getPropertyShapesOfType(t *Type) []*ast.Symbol { t = c.getReducedApparentType(t) if t.flags&TypeFlagsObject != 0 { - resolved := c.resolveStructuredTypeMembers(t) - if resolved.properties == nil && resolved.lazy != nil { - return resolved.lazy.layout.symbols[:resolved.lazy.layout.namedCount], resolved.lazy - } - return resolved.properties, nil + return c.resolveStructuredTypeMembers(t).propertyShapes() } - return c.getPropertiesOfType(t), nil + return c.getPropertiesOfType(t) } -func (c *Checker) getPropertyCountOfType(t *Type) int { +// Returns getPropertiesOfType(t)[i], resolving only that property. +func (c *Checker) getPropertyOfTypeAt(t *Type, i int) *ast.Symbol { t = c.getReducedApparentType(t) if t.flags&TypeFlagsObject != 0 { - return c.resolveStructuredTypeMembers(t).propertyCount() + return c.resolveStructuredTypeMembers(t).property(i) } - return len(c.getPropertiesOfType(t)) + return c.getPropertiesOfType(t)[i] } func (c *Checker) getPropertiesOfObjectType(t *Type) []*ast.Symbol { @@ -19551,14 +19546,12 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters var callSignatures []*Signature var constructSignatures []*Signature var indexInfos []*IndexInfo - var instantiated bool resolved := c.resolveDeclaredMembers(source) if slices.Equal(typeParameters, typeArguments) { callSignatures = resolved.declaredCallSignatures constructSignatures = resolved.declaredConstructSignatures indexInfos = resolved.declaredIndexInfos } else { - instantiated = true mapper = newTypeMapper(typeParameters, typeArguments) callSignatures = c.instantiateSignatures(resolved.declaredCallSignatures, mapper) constructSignatures = c.instantiateSignatures(resolved.declaredConstructSignatures, mapper) @@ -19593,35 +19586,39 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters return findIndexInfo(indexInfos, info.keyType) == nil })) } - c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes, instantiated: instantiated}, callSignatures, constructSignatures, indexInfos) + c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) } -const ( - memberOriginOwn int32 = -1 - memberOriginFixed int32 = -2 - memberSlotByName int32 = -1 -) +// A memberLayout describes the members of a class or interface type and of its instantiations: +// their keys, their order, and where each one comes from. Member keys and order don't depend on +// type arguments when every base type has a shared layout, in which case the layout is computed +// once per class or interface and shared by every instantiation. +type memberLayout struct { + symbols []*ast.Symbol // Declared or inherited symbols, with the names and flags of every instantiation's members + keys []string // Member keys, or nil when every key is its symbol's name + origins []memberOrigin + index []uint32 // Open-addressed hash table of 1-based member indices, or nil for small layouts + propertyCount int // The properties are the first members, in property order + shared bool +} -// A memberOrigin says where the member in a layout slot comes from: a declared member of the -// source type (instantiated with the instance mapper), a slot of the layout of a base type, the -// property with the same name in a base type without a layout, or a fixed symbol. type memberOrigin struct { - base int32 - slot int32 + kind memberOriginKind + base int32 // Index of the base type of an inherited member + slot int32 // Index of the member in the base type's layout, for memberOriginBaseLayout } -// A memberLayout describes the members of a class or interface type and of all of its -// instantiations: their keys, their order, and where each one comes from. Named members come -// first, in property order. Member names and property order do not depend on type arguments -// when every base type has a shared layout, in which case the layout is computed once per -// class or interface and shared by every instantiation. -type memberLayout struct { - keys []string // Member keys, nil when every key is the symbol name - symbols []*ast.Symbol // Declared member, representative base member, or fixed member - origins []memberOrigin - index []uint32 // Open-addressed hash of 1-based slot numbers, nil for small layouts - namedCount int - shared bool +type memberOriginKind uint8 + +const ( + memberOriginDeclared memberOriginKind = iota // Declared member, instantiated with the type's mapper + memberOriginBaseLayout // Member of the base type, through the base type's layout + memberOriginBaseName // Property of the base type with the same name + memberOriginFixed // The symbol itself, in a layout that isn't shared +) + +func (l *memberLayout) properties() []*ast.Symbol { + return l.symbols[:l.propertyCount] } func (l *memberLayout) key(i int) string { @@ -19641,90 +19638,114 @@ func (l *memberLayout) find(key string) int { return -1 } mask := uint64(len(l.index) - 1) - slot := xxh3.HashString(key) & mask - for { - i := l.index[slot] - if i == 0 { - return -1 + for h := xxh3.HashString(key) & mask; l.index[h] != 0; h = (h + 1) & mask { + if i := int(l.index[h] - 1); l.key(i) == key { + return i } - if l.key(int(i-1)) == key { - return int(i - 1) + } + return -1 +} + +// A map index would take several times the memory, and there are many large layouts +// because every interface layout includes all of its inherited members. +func (l *memberLayout) buildIndex() { + if len(l.symbols) <= 8 { + return + } + l.index = make([]uint32, 1<= l.layout.namedCount { - return nil + if i := l.layout.find(name); i >= 0 && i < l.memberCount() { + return c.getLazyMember(l, i) } - return c.getLazyMember(l, i) + return nil } func (c *Checker) getLazyProperties(l *lazyMembers) []*ast.Symbol { - n := l.layout.namedCount + n := l.layout.propertyCount if n == 0 { return nil } @@ -19902,10 +19899,7 @@ func (c *Checker) getLazyProperties(l *lazyMembers) []*ast.Symbol { } func (c *Checker) getLazyMemberTable(l *lazyMembers) ast.SymbolTable { - n := len(l.layout.symbols) - if l.instantiated { - n = l.layout.namedCount - } + n := l.memberCount() if n == 0 { return nil } @@ -20114,7 +20108,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 || resolved.propertyCount() == 0 && len(resolved.indexInfos) == 0 { + if allowMembers || len(resolved.propertyShapes()) == 0 && len(resolved.indexInfos) == 0 { if kind == SignatureKindCall && len(resolved.CallSignatures()) == 1 && len(resolved.ConstructSignatures()) == 0 { return resolved.CallSignatures()[0] } @@ -22637,8 +22631,7 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { // Collect declaration counts for each property across all constituent types of the intersection. var counts collections.OrderedMap[string, int] for _, t := range t.Types() { - shapes, _ := c.getPropertyShapesOfType(t) - for _, prop := range shapes { + for _, prop := range c.getPropertyShapesOfType(t) { counts.Set(prop.Name(), counts.GetOrZero(prop.Name())+1) } } @@ -27367,7 +27360,7 @@ func (c *Checker) IsEmptyAnonymousObjectType(t *Type) bool { } func (c *Checker) isEmptyResolvedType(t *StructuredType) bool { - return t.AsType() != c.anyFunctionType && t.propertyCount() == 0 && len(t.signatures) == 0 && len(t.indexInfos) == 0 + return t.AsType() != c.anyFunctionType && len(t.propertyShapes()) == 0 && len(t.signatures) == 0 && len(t.indexInfos) == 0 } func (c *Checker) isEmptyObjectType(t *Type) bool { @@ -28251,7 +28244,7 @@ func (c *Checker) getPropertyNameFromIndex(indexType *Type, accessNode *ast.Node } func (c *Checker) isStringIndexSignatureOnlyTypeWorker(t *Type) bool { - return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && c.getPropertyCountOfType(t) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || + return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && len(c.getPropertyShapesOfType(t)) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || t.flags&TypeFlagsUnionOrIntersection != 0 && core.Every(t.Types(), c.isStringIndexSignatureOnlyType) } diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 46620a34bd36a..d26abd61efb66 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -679,7 +679,10 @@ 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 && resolved.propertyCount() > 0 && resolved.everyPropertyHasFlags(ast.SymbolFlagsOptional) + properties := resolved.propertyShapes() + return len(resolved.signatures) == 0 && len(resolved.indexInfos) == 0 && len(properties) > 0 && core.Every(properties, func(p *ast.Symbol) bool { + return p.Flags()&ast.SymbolFlagsOptional != 0 + }) } if t.flags&TypeFlagsSubstitution != 0 { return c.isWeakType(t.AsSubstitutionType().baseType) @@ -974,16 +977,17 @@ func (c *Checker) getUnmatchedProperties(source *Type, target *Type, requireOpti func (c *Checker) getUnmatchedPropertiesWorker(source *Type, target *Type, requireOptionalProperties bool, matchDiscriminantProperties bool, propsOut *[]*ast.Symbol) *ast.Symbol { // Names and flags of properties are known without resolving them, so only resolve the ones we report or compare. - shapes, lazy := c.getPropertyShapesOfType(target) - for i, targetProp := range shapes { + // Reduce the target once, since getting its apparent type can allocate. + target = c.getReducedApparentType(target) + for i, targetProp := range c.getPropertyShapesOfType(target) { // TODO: remove this when we support static private identifier fields and find other solutions to get privateNamesAndStaticFields test to pass if isStaticPrivateIdentifierProperty(targetProp) { continue } if requireOptionalProperties || targetProp.Flags()&ast.SymbolFlagsOptional == 0 && targetProp.CheckFlags()&ast.CheckFlagsPartial == 0 { sourceProp := c.getPropertyOfType(source, targetProp.Name()) - if lazy != nil && (sourceProp == nil || matchDiscriminantProperties) { - targetProp = c.getLazyMember(lazy, i) + if sourceProp == nil || matchDiscriminantProperties { + targetProp = c.getPropertyOfTypeAt(target, i) } if sourceProp == nil { if propsOut == nil { diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 6b8d2e322723a..39544ca00ae81 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -961,27 +961,19 @@ func (t *StructuredType) Properties() []*ast.Symbol { return t.properties } -// Returns the number of properties without resolving them. -func (t *StructuredType) propertyCount() int { +// Like Properties, but returns symbols with only the right names and flags, without resolving lazy members. +func (t *StructuredType) propertyShapes() []*ast.Symbol { if t.properties == nil && t.lazy != nil { - return t.lazy.layout.namedCount + return t.lazy.layout.properties() } - return len(t.properties) + return t.properties } -// Reports whether every property has the given symbol flags, without resolving the properties. Instantiation -// preserves symbol flags, so the layout's representative symbols answer this for every instantiation. -func (t *StructuredType) everyPropertyHasFlags(flags ast.SymbolFlags) bool { - properties := t.properties - if properties == nil && t.lazy != nil { - properties = t.lazy.layout.symbols[:t.lazy.layout.namedCount] - } - for _, p := range properties { - if p.Flags()&flags == 0 { - return false - } +func (t *StructuredType) property(i int) *ast.Symbol { + if t.properties == nil && t.lazy != nil { + return t.lazy.c.getLazyMember(t.lazy, i) } - return true + return t.properties[i] } func (t *StructuredType) getMember(name string) *ast.Symbol { From a87c1dc469412bb1f92c4377387345047110fc24 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:50:56 -0700 Subject: [PATCH 05/28] Account for lazy members in the structured type size check Lazily resolved members add one pointer to every structured type. --- tsc/internal/checker/types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..763b8325401ce 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -509,7 +509,7 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) { func TestCompactStructuredTypeLayout(t *testing.T) { t.Parallel() - assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0))) + assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+6*unsafe.Sizeof(uintptr(0))) } func TestStructuredTypeExtra(t *testing.T) { From 281ff134598fb9fe87789923f6701189870d6822 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:59:54 -0700 Subject: [PATCH 06/28] Add test for namespace imports of callable export= values Namespace imports of a callable export= value whose members are resolved lazily lose every property, so ns.value reports a missing property. This test captures the current, incorrect output. --- ...aceExport(esmoduleinterop=true).errors.txt | 25 +++++++++++ ...erfaceExport(esmoduleinterop=true).symbols | 41 +++++++++++++++++++ ...nterfaceExport(esmoduleinterop=true).types | 38 +++++++++++++++++ ...amespaceImportOfCallableInterfaceExport.ts | 20 +++++++++ 4 files changed, 124 insertions(+) create mode 100644 tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols create mode 100644 tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types create mode 100644 tsc/testdata/tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt new file mode 100644 index 0000000000000..dcb1a4cad3773 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt @@ -0,0 +1,25 @@ +main.ts(2,26): error TS2339: Property 'value' does not exist on type '{}'. +main.ts(3,26): error TS2339: Property 'other' does not exist on type '{}'. + + +==== mod.d.ts (0 errors) ==== + interface Base { + value: T; + } + interface Callable extends Base { + (): void; + __esModule: true; + other: number; + } + declare const callable: Callable; + export = callable; + +==== main.ts (2 errors) ==== + import * as ns from "./mod"; + const value: string = ns.value; + ~~~~~ +!!! error TS2339: Property 'value' does not exist on type '{}'. + const other: number = ns.other; + ~~~~~ +!!! error TS2339: Property 'other' does not exist on type '{}'. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols new file mode 100644 index 0000000000000..eb72097e9d6f1 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols @@ -0,0 +1,41 @@ +//// [tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts] //// + +=== mod.d.ts === +interface Base { +>Base : Symbol(Base, Decl(mod.d.ts, 0, 0)) +>T : Symbol(T, Decl(mod.d.ts, 0, 15)) + + value: T; +>value : Symbol(Base.value, Decl(mod.d.ts, 0, 19)) +>T : Symbol(T, Decl(mod.d.ts, 0, 15)) +} +interface Callable extends Base { +>Callable : Symbol(Callable, Decl(mod.d.ts, 2, 1)) +>Base : Symbol(Base, Decl(mod.d.ts, 0, 0)) + + (): void; + __esModule: true; +>__esModule : Symbol(Callable.__esModule, Decl(mod.d.ts, 4, 13)) + + other: number; +>other : Symbol(Callable.other, Decl(mod.d.ts, 5, 21)) +} +declare const callable: Callable; +>callable : Symbol(callable, Decl(mod.d.ts, 8, 13)) +>Callable : Symbol(Callable, Decl(mod.d.ts, 2, 1)) + +export = callable; +>callable : Symbol(callable, Decl(mod.d.ts, 8, 13)) + +=== main.ts === +import * as ns from "./mod"; +>ns : Symbol(ns, Decl(main.ts, 0, 6)) + +const value: string = ns.value; +>value : Symbol(value, Decl(main.ts, 1, 5)) +>ns : Symbol(ns, Decl(main.ts, 0, 6)) + +const other: number = ns.other; +>other : Symbol(other, Decl(main.ts, 2, 5)) +>ns : Symbol(ns, Decl(main.ts, 0, 6)) + diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types new file mode 100644 index 0000000000000..a82b012a7f01b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types @@ -0,0 +1,38 @@ +//// [tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts] //// + +=== mod.d.ts === +interface Base { + value: T; +>value : T +} +interface Callable extends Base { + (): void; + __esModule: true; +>__esModule : true +>true : true + + other: number; +>other : number +} +declare const callable: Callable; +>callable : Callable + +export = callable; +>callable : Callable + +=== main.ts === +import * as ns from "./mod"; +>ns : {} + +const value: string = ns.value; +>value : string +>ns.value : any +>ns : {} +>value : any + +const other: number = ns.other; +>other : number +>ns.other : any +>ns : {} +>other : any + diff --git a/tsc/testdata/tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts b/tsc/testdata/tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts new file mode 100644 index 0000000000000..2b587f0660178 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/namespaceImportOfCallableInterfaceExport.ts @@ -0,0 +1,20 @@ +// @module: commonjs +// @esModuleInterop: true,false +// @noEmit: true + +// @filename: mod.d.ts +interface Base { + value: T; +} +interface Callable extends Base { + (): void; + __esModule: true; + other: number; +} +declare const callable: Callable; +export = callable; + +// @filename: main.ts +import * as ns from "./mod"; +const value: string = ns.value; +const other: number = ns.other; From 32efca39ccef9aa22c3d9ebe7d5b56f73c109dd2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:00:01 -0700 Subject: [PATCH 07/28] Materialize lazy members when cloning a module type Members resolved through a shared layout leave the members table nil until it is requested, so reading the field directly cloned an empty table for namespace imports of callable export= values. --- tsc/internal/checker/checker.go | 2 +- ...aceExport(esmoduleinterop=true).errors.txt | 25 ------------------- ...erfaceExport(esmoduleinterop=true).symbols | 4 +++ ...nterfaceExport(esmoduleinterop=true).types | 14 +++++------ 4 files changed, 12 insertions(+), 33 deletions(-) delete mode 100644 tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 13c6ed7e86303..87768304d87c8 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -16141,7 +16141,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 } diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt deleted file mode 100644 index dcb1a4cad3773..0000000000000 --- a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).errors.txt +++ /dev/null @@ -1,25 +0,0 @@ -main.ts(2,26): error TS2339: Property 'value' does not exist on type '{}'. -main.ts(3,26): error TS2339: Property 'other' does not exist on type '{}'. - - -==== mod.d.ts (0 errors) ==== - interface Base { - value: T; - } - interface Callable extends Base { - (): void; - __esModule: true; - other: number; - } - declare const callable: Callable; - export = callable; - -==== main.ts (2 errors) ==== - import * as ns from "./mod"; - const value: string = ns.value; - ~~~~~ -!!! error TS2339: Property 'value' does not exist on type '{}'. - const other: number = ns.other; - ~~~~~ -!!! error TS2339: Property 'other' does not exist on type '{}'. - \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols index eb72097e9d6f1..b68cf3b721b9d 100644 --- a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols +++ b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).symbols @@ -33,9 +33,13 @@ import * as ns from "./mod"; const value: string = ns.value; >value : Symbol(value, Decl(main.ts, 1, 5)) +>ns.value : Symbol(Base.value, Decl(mod.d.ts, 0, 19)) >ns : Symbol(ns, Decl(main.ts, 0, 6)) +>value : Symbol(Base.value, Decl(mod.d.ts, 0, 19)) const other: number = ns.other; >other : Symbol(other, Decl(main.ts, 2, 5)) +>ns.other : Symbol(Callable.other, Decl(mod.d.ts, 5, 21)) >ns : Symbol(ns, Decl(main.ts, 0, 6)) +>other : Symbol(Callable.other, Decl(mod.d.ts, 5, 21)) diff --git a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types index a82b012a7f01b..c94785f4cd666 100644 --- a/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types +++ b/tsc/testdata/baselines/reference/compiler/namespaceImportOfCallableInterfaceExport(esmoduleinterop=true).types @@ -22,17 +22,17 @@ export = callable; === main.ts === import * as ns from "./mod"; ->ns : {} +>ns : { value: string; __esModule: true; other: number; } const value: string = ns.value; >value : string ->ns.value : any ->ns : {} ->value : any +>ns.value : string +>ns : { value: string; __esModule: true; other: number; } +>value : string const other: number = ns.other; >other : number ->ns.other : any ->ns : {} ->other : any +>ns.other : number +>ns : { value: string; __esModule: true; other: number; } +>other : number From ff5b586d1b8aaeafe23155a70ad7701fd40c3c12 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:00:17 -0700 Subject: [PATCH 08/28] Test inherited member lookups before enumeration Members of class and interface instantiations are now resolved one at a time through shared layouts, so a lookup of a single inherited member can happen before the full member list is built. Cover lookups through layout and non-layout bases, followed by enumeration, to check that the members and their order match eager resolution. --- .../lazyInheritedMemberLookup.symbols | 146 ++++++++++++++++++ .../compiler/lazyInheritedMemberLookup.types | 109 +++++++++++++ .../compiler/lazyInheritedMemberLookup.ts | 41 +++++ 3 files changed, 296 insertions(+) create mode 100644 tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.types create mode 100644 tsc/testdata/tests/cases/compiler/lazyInheritedMemberLookup.ts diff --git a/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.symbols b/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.symbols new file mode 100644 index 0000000000000..abe1b21713175 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.symbols @@ -0,0 +1,146 @@ +//// [tests/cases/compiler/lazyInheritedMemberLookup.ts] //// + +=== lazyInheritedMemberLookup.ts === +// Inherited members are looked up individually before the members of each type are enumerated. + +interface Grand { +>Grand : Symbol(Grand, Decl(lazyInheritedMemberLookup.ts, 0, 0)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 2, 16)) + + grand: T; +>grand : Symbol(Grand.grand, Decl(lazyInheritedMemberLookup.ts, 2, 20)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 2, 16)) + + shared: T; +>shared : Symbol(Grand.shared, Decl(lazyInheritedMemberLookup.ts, 3, 13)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 2, 16)) +} +type Mixin = { mixin: T }; +>Mixin : Symbol(Mixin, Decl(lazyInheritedMemberLookup.ts, 5, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 6, 11)) +>mixin : Symbol(mixin, Decl(lazyInheritedMemberLookup.ts, 6, 17)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 6, 11)) + +type Fixed = { fixed: string }; +>Fixed : Symbol(Fixed, Decl(lazyInheritedMemberLookup.ts, 6, 29)) +>fixed : Symbol(fixed, Decl(lazyInheritedMemberLookup.ts, 7, 14)) + +interface Base extends Grand { +>Base : Symbol(Base, Decl(lazyInheritedMemberLookup.ts, 7, 31)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 8, 15)) +>Grand : Symbol(Grand, Decl(lazyInheritedMemberLookup.ts, 0, 0)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 8, 15)) + + base: T; +>base : Symbol(Base.base, Decl(lazyInheritedMemberLookup.ts, 8, 38)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 8, 15)) +} +interface Derived extends Base, Mixin, Fixed { +>Derived : Symbol(Derived, Decl(lazyInheritedMemberLookup.ts, 10, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 11, 18)) +>Base : Symbol(Base, Decl(lazyInheritedMemberLookup.ts, 7, 31)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 11, 18)) +>Mixin : Symbol(Mixin, Decl(lazyInheritedMemberLookup.ts, 5, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 11, 18)) +>Fixed : Symbol(Fixed, Decl(lazyInheritedMemberLookup.ts, 6, 29)) + + shared: T[]; +>shared : Symbol(Derived.shared, Decl(lazyInheritedMemberLookup.ts, 11, 55)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 11, 18)) + + derived: T; +>derived : Symbol(Derived.derived, Decl(lazyInheritedMemberLookup.ts, 12, 16)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 11, 18)) +} +class C { +>C : Symbol(C, Decl(lazyInheritedMemberLookup.ts, 14, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 15, 8)) + + declared!: T; +>declared : Symbol(C.declared, Decl(lazyInheritedMemberLookup.ts, 15, 12)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 15, 8)) +} +class D extends C implements Derived { +>D : Symbol(D, Decl(lazyInheritedMemberLookup.ts, 17, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) +>C : Symbol(C, Decl(lazyInheritedMemberLookup.ts, 14, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) +>Derived : Symbol(Derived, Decl(lazyInheritedMemberLookup.ts, 10, 1)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) + + grand!: T[]; +>grand : Symbol(D.grand, Decl(lazyInheritedMemberLookup.ts, 18, 49)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) + + shared!: T[]; +>shared : Symbol(D.shared, Decl(lazyInheritedMemberLookup.ts, 19, 16)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) + + mixin!: T; +>mixin : Symbol(D.mixin, Decl(lazyInheritedMemberLookup.ts, 20, 17)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) + + fixed!: string; +>fixed : Symbol(D.fixed, Decl(lazyInheritedMemberLookup.ts, 21, 14)) + + base!: T; +>base : Symbol(D.base, Decl(lazyInheritedMemberLookup.ts, 22, 19)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) + + derived!: T; +>derived : Symbol(D.derived, Decl(lazyInheritedMemberLookup.ts, 23, 13)) +>T : Symbol(T, Decl(lazyInheritedMemberLookup.ts, 18, 8)) +} + +declare const d: Derived; +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) +>Derived : Symbol(Derived, Decl(lazyInheritedMemberLookup.ts, 10, 1)) + +const grand = d.grand; +>grand : Symbol(grand, Decl(lazyInheritedMemberLookup.ts, 28, 5)) +>d.grand : Symbol(Grand.grand, Decl(lazyInheritedMemberLookup.ts, 2, 20)) +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) +>grand : Symbol(Grand.grand, Decl(lazyInheritedMemberLookup.ts, 2, 20)) + +const mixin = d.mixin; +>mixin : Symbol(mixin, Decl(lazyInheritedMemberLookup.ts, 29, 5)) +>d.mixin : Symbol(mixin, Decl(lazyInheritedMemberLookup.ts, 6, 17)) +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) +>mixin : Symbol(mixin, Decl(lazyInheritedMemberLookup.ts, 6, 17)) + +const fixed = d.fixed; +>fixed : Symbol(fixed, Decl(lazyInheritedMemberLookup.ts, 30, 5)) +>d.fixed : Symbol(fixed, Decl(lazyInheritedMemberLookup.ts, 7, 14)) +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) +>fixed : Symbol(fixed, Decl(lazyInheritedMemberLookup.ts, 7, 14)) + +const shared = d.shared; +>shared : Symbol(shared, Decl(lazyInheritedMemberLookup.ts, 31, 5)) +>d.shared : Symbol(Derived.shared, Decl(lazyInheritedMemberLookup.ts, 11, 55)) +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) +>shared : Symbol(Derived.shared, Decl(lazyInheritedMemberLookup.ts, 11, 55)) + +const all = { ...d }; +>all : Symbol(all, Decl(lazyInheritedMemberLookup.ts, 32, 5)) +>d : Symbol(d, Decl(lazyInheritedMemberLookup.ts, 27, 13)) + +declare const keys: { [K in keyof Derived]: K }; +>keys : Symbol(keys, Decl(lazyInheritedMemberLookup.ts, 33, 13)) +>K : Symbol(K, Decl(lazyInheritedMemberLookup.ts, 33, 23)) +>Derived : Symbol(Derived, Decl(lazyInheritedMemberLookup.ts, 10, 1)) +>K : Symbol(K, Decl(lazyInheritedMemberLookup.ts, 33, 23)) + +declare const e: D; +>e : Symbol(e, Decl(lazyInheritedMemberLookup.ts, 35, 13)) +>D : Symbol(D, Decl(lazyInheritedMemberLookup.ts, 17, 1)) + +const declared = e.declared; +>declared : Symbol(declared, Decl(lazyInheritedMemberLookup.ts, 36, 5)) +>e.declared : Symbol(C.declared, Decl(lazyInheritedMemberLookup.ts, 15, 12)) +>e : Symbol(e, Decl(lazyInheritedMemberLookup.ts, 35, 13)) +>declared : Symbol(C.declared, Decl(lazyInheritedMemberLookup.ts, 15, 12)) + +const allE = { ...e }; +>allE : Symbol(allE, Decl(lazyInheritedMemberLookup.ts, 37, 5)) +>e : Symbol(e, Decl(lazyInheritedMemberLookup.ts, 35, 13)) + diff --git a/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.types b/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.types new file mode 100644 index 0000000000000..54cf76203287b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lazyInheritedMemberLookup.types @@ -0,0 +1,109 @@ +//// [tests/cases/compiler/lazyInheritedMemberLookup.ts] //// + +=== lazyInheritedMemberLookup.ts === +// Inherited members are looked up individually before the members of each type are enumerated. + +interface Grand { + grand: T; +>grand : T + + shared: T; +>shared : T +} +type Mixin = { mixin: T }; +>Mixin : Mixin +>mixin : T + +type Fixed = { fixed: string }; +>Fixed : Fixed +>fixed : string + +interface Base extends Grand { + base: T; +>base : T +} +interface Derived extends Base, Mixin, Fixed { + shared: T[]; +>shared : T[] + + derived: T; +>derived : T +} +class C { +>C : C + + declared!: T; +>declared : T +} +class D extends C implements Derived { +>D : D +>C : C + + grand!: T[]; +>grand : T[] + + shared!: T[]; +>shared : T[] + + mixin!: T; +>mixin : T + + fixed!: string; +>fixed : string + + base!: T; +>base : T + + derived!: T; +>derived : T +} + +declare const d: Derived; +>d : Derived + +const grand = d.grand; +>grand : number[] +>d.grand : number[] +>d : Derived +>grand : number[] + +const mixin = d.mixin; +>mixin : number +>d.mixin : number +>d : Derived +>mixin : number + +const fixed = d.fixed; +>fixed : string +>d.fixed : string +>d : Derived +>fixed : string + +const shared = d.shared; +>shared : number[] +>d.shared : number[] +>d : Derived +>shared : number[] + +const all = { ...d }; +>all : { grand: number[]; mixin: number; fixed: string; base: number; shared: number[]; derived: number; } +>{ ...d } : { grand: number[]; mixin: number; fixed: string; base: number; shared: number[]; derived: number; } +>d : Derived + +declare const keys: { [K in keyof Derived]: K }; +>keys : { grand: "grand"; mixin: "mixin"; fixed: "fixed"; base: "base"; shared: "shared"; derived: "derived"; } + +declare const e: D; +>e : D + +const declared = e.declared; +>declared : string[] +>e.declared : string[] +>e : D +>declared : string[] + +const allE = { ...e }; +>allE : { declared: string[]; grand: string[]; shared: string[]; mixin: string; fixed: string; base: string; derived: string; } +>{ ...e } : { declared: string[]; grand: string[]; shared: string[]; mixin: string; fixed: string; base: string; derived: string; } +>e : D + diff --git a/tsc/testdata/tests/cases/compiler/lazyInheritedMemberLookup.ts b/tsc/testdata/tests/cases/compiler/lazyInheritedMemberLookup.ts new file mode 100644 index 0000000000000..87beb549f9766 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/lazyInheritedMemberLookup.ts @@ -0,0 +1,41 @@ +// @strict: true +// @noEmit: true + +// Inherited members are looked up individually before the members of each type are enumerated. + +interface Grand { + grand: T; + shared: T; +} +type Mixin = { mixin: T }; +type Fixed = { fixed: string }; +interface Base extends Grand { + base: T; +} +interface Derived extends Base, Mixin, Fixed { + shared: T[]; + derived: T; +} +class C { + declared!: T; +} +class D extends C implements Derived { + grand!: T[]; + shared!: T[]; + mixin!: T; + fixed!: string; + base!: T; + derived!: T; +} + +declare const d: Derived; +const grand = d.grand; +const mixin = d.mixin; +const fixed = d.fixed; +const shared = d.shared; +const all = { ...d }; +declare const keys: { [K in keyof Derived]: K }; + +declare const e: D; +const declared = e.declared; +const allE = { ...e }; From f4c121e6e66fb8ff81e42cfd69914a0d7d2d1b9d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:00:17 -0700 Subject: [PATCH 09/28] Test member layouts after a base hits the instantiation limit When a base type's own base fails to instantiate, the base gets a smaller layout of its own, but the derived type still reuses slot indices from the shared layout. The derived type then reports the wrong members. This test captures the current, incorrect output. --- tsc/internal/checker/checker_test.go | 36 ++++++++++++++++++++++++++++ tsc/internal/checker/export_test.go | 13 ++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tsc/internal/checker/export_test.go diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 31cea19751525..70bfb3b67a453 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -208,3 +208,39 @@ export type Initial = Box<"first">; assert.Equal(t, c.TypeToString(initial), "Initial") assert.Equal(t, len(c.GetDiagnostics(t.Context(), file)), 0) } + +func TestMemberLayoutAfterBaseInstantiationLimit(t *testing.T) { + t.Parallel() + fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ + "/foo.ts": `interface Grand { grand: T } +interface Base extends Grand { base: T } +interface Derived extends Base { derived: T } +export type S = Derived; +export type N = Derived; +export type B = Base; +`, + }, tspath.CaseSensitive)) + opts := &core.CompilerOptions{NoLib: core.TSTrue} + program := compiler.NewProgram(compiler.ProgramOptions{ + Config: tsoptions.NewParsedCommandLine(opts, []tspath.RootedFilePath{"/foo.ts"}, nil, "/", fs.CaseSensitivity()), + Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), + }) + program.BindSourceFiles() + c, done := program.GetTypeChecker(t.Context()) + defer done() + file := program.GetSourceFile("/foo.ts") + typeOfAlias := func(i int) *checker.Type { + return c.GetTypeAtLocation(file.Statements.Nodes[i].AsTypeAliasDeclaration().Type) + } + // Share the member layouts of Derived, Base, and Grand. + assert.Equal(t, len(c.GetPropertiesOfType(typeOfAlias(3))), 3) + // Resolve the base of Derived after instantiating Grand has hit the limit. + derived := typeOfAlias(4) + base := c.GetTypeWithThisArgument(typeOfAlias(5), derived) + c.RunAtInstantiationLimit(func() { + assert.Equal(t, len(c.GetPropertiesOfType(base)), 1) + }) + // BUG: Derived reuses slots from the shared layout of Base, which this base doesn't have. + names := core.Map(c.GetPropertiesOfType(derived), (*ast.Symbol).Name) + assert.DeepEqual(t, names, []string{"derived", "T", "base"}) +} diff --git a/tsc/internal/checker/export_test.go b/tsc/internal/checker/export_test.go new file mode 100644 index 0000000000000..22b9b44ad5950 --- /dev/null +++ b/tsc/internal/checker/export_test.go @@ -0,0 +1,13 @@ +package checker + +// RunAtInstantiationLimit runs f as if the current statement had reached the instantiation count limit. +func (c *Checker) RunAtInstantiationLimit(f func()) { + saved := c.instantiationCount + c.instantiationCount = 5_000_000 + defer func() { c.instantiationCount = saved }() + f() +} + +func (c *Checker) GetTypeWithThisArgument(t *Type, thisArgument *Type) *Type { + return c.getTypeWithThisArgument(t, thisArgument, false /*needsApparentType*/) +} From 0b2c03df2968dbee0852fe54f3718165fbaaab47 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:04:15 -0700 Subject: [PATCH 10/28] Rebuild member layouts when any base layout isn't shared A shared layout addresses inherited members by their slots in each base's shared layout. Checking only for error bases missed a base whose own base hit the instantiation limit: that base gets a smaller layout, so the derived type's slots pointed at the wrong members or past the end of the base's members. --- tsc/internal/checker/checker.go | 25 ++++++++++++++++++++++--- tsc/internal/checker/checker_test.go | 3 +-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 87768304d87c8..793578b4dfcb4 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19705,14 +19705,33 @@ func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { } return layout } - // A base type instantiation that hit an instantiation limit is the error type, which has - // none of the members the shared layout expects to find in it. - if slices.Contains(bases, c.errorType) && !slices.Contains(c.getBaseTypes(source), c.errorType) { + if !c.canShareMemberLayout(source, bases) { return c.buildMemberLayout(source, bases) } return d.memberLayout } +// Reports whether the bases of an instantiation have the members the shared layout expects. A base +// type instantiation that hit an instantiation limit is the error type, which has no members, and +// a base whose own base hit the limit has a smaller layout that isn't shared. +func (c *Checker) canShareMemberLayout(source *Type, bases []*Type) bool { + declaredBases := c.getBaseTypes(source) + for k, base := range bases { + if base == c.errorType { + if declaredBases[k] != c.errorType { + return false + } + continue + } + if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { + if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil && !lazy.layout.shared { + return false + } + } + } + return true +} + type layoutEntry struct { key string symbol *ast.Symbol diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 70bfb3b67a453..542db57875e71 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -240,7 +240,6 @@ export type B = Base; c.RunAtInstantiationLimit(func() { assert.Equal(t, len(c.GetPropertiesOfType(base)), 1) }) - // BUG: Derived reuses slots from the shared layout of Base, which this base doesn't have. names := core.Map(c.GetPropertiesOfType(derived), (*ast.Symbol).Name) - assert.DeepEqual(t, names, []string{"derived", "T", "base"}) + assert.DeepEqual(t, names, []string{"derived", "base"}) } From 524eedccc80769d0bb4eda28de1d97c17dc7bb11 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:02:26 -0700 Subject: [PATCH 11/28] Test member layouts of interfaces extending variadic tuples Instantiations of a variadic tuple base can have different tuple targets, and so different member layouts. The derived interface still reuses the layout built for its first instantiation, so it loses the added tuple elements. This test captures the current, incorrect output. --- .../sharedMemberLayoutTupleBases.errors.txt | 18 ++++++++ .../sharedMemberLayoutTupleBases.symbols | 42 +++++++++++++++++++ .../sharedMemberLayoutTupleBases.types | 38 +++++++++++++++++ .../compiler/sharedMemberLayoutTupleBases.ts | 14 +++++++ 4 files changed, 112 insertions(+) create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types create mode 100644 tsc/testdata/tests/cases/compiler/sharedMemberLayoutTupleBases.ts diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.errors.txt b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.errors.txt new file mode 100644 index 0000000000000..a75a8eb9fe3b8 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.errors.txt @@ -0,0 +1,18 @@ +sharedMemberLayoutTupleBases.ts(4,11): error TS2430: Interface 'FromTuple' incorrectly extends interface '[number, ...T]'. + + +==== sharedMemberLayoutTupleBases.ts (1 errors) ==== + // Each tuple base can have a different target, and so a different member layout. + + type Prefixed = [number, ...T]; + interface FromTuple extends Prefixed {} + ~~~~~~~~~ +!!! error TS2430: Interface 'FromTuple' incorrectly extends interface '[number, ...T]'. + declare const generic: FromTuple; + const genericFirst = generic[0]; + const genericAll = { ...generic }; + declare const fixed: FromTuple<[string]>; + const fixedSecond = fixed[1]; + const fixedAll = { ...fixed }; + + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols new file mode 100644 index 0000000000000..2333b1b223504 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols @@ -0,0 +1,42 @@ +//// [tests/cases/compiler/sharedMemberLayoutTupleBases.ts] //// + +=== sharedMemberLayoutTupleBases.ts === +// Each tuple base can have a different target, and so a different member layout. + +type Prefixed = [number, ...T]; +>Prefixed : Symbol(Prefixed, Decl(sharedMemberLayoutTupleBases.ts, 0, 0)) +>T : Symbol(T, Decl(sharedMemberLayoutTupleBases.ts, 2, 14)) +>T : Symbol(T, Decl(sharedMemberLayoutTupleBases.ts, 2, 14)) + +interface FromTuple extends Prefixed {} +>FromTuple : Symbol(FromTuple, Decl(sharedMemberLayoutTupleBases.ts, 2, 52)) +>T : Symbol(T, Decl(sharedMemberLayoutTupleBases.ts, 3, 20)) +>Prefixed : Symbol(Prefixed, Decl(sharedMemberLayoutTupleBases.ts, 0, 0)) +>T : Symbol(T, Decl(sharedMemberLayoutTupleBases.ts, 3, 20)) + +declare const generic: FromTuple; +>generic : Symbol(generic, Decl(sharedMemberLayoutTupleBases.ts, 4, 13)) +>FromTuple : Symbol(FromTuple, Decl(sharedMemberLayoutTupleBases.ts, 2, 52)) + +const genericFirst = generic[0]; +>genericFirst : Symbol(genericFirst, Decl(sharedMemberLayoutTupleBases.ts, 5, 5)) +>generic : Symbol(generic, Decl(sharedMemberLayoutTupleBases.ts, 4, 13)) +>0 : Symbol(0) + +const genericAll = { ...generic }; +>genericAll : Symbol(genericAll, Decl(sharedMemberLayoutTupleBases.ts, 6, 5)) +>generic : Symbol(generic, Decl(sharedMemberLayoutTupleBases.ts, 4, 13)) + +declare const fixed: FromTuple<[string]>; +>fixed : Symbol(fixed, Decl(sharedMemberLayoutTupleBases.ts, 7, 13)) +>FromTuple : Symbol(FromTuple, Decl(sharedMemberLayoutTupleBases.ts, 2, 52)) + +const fixedSecond = fixed[1]; +>fixedSecond : Symbol(fixedSecond, Decl(sharedMemberLayoutTupleBases.ts, 8, 5)) +>fixed : Symbol(fixed, Decl(sharedMemberLayoutTupleBases.ts, 7, 13)) + +const fixedAll = { ...fixed }; +>fixedAll : Symbol(fixedAll, Decl(sharedMemberLayoutTupleBases.ts, 9, 5)) +>fixed : Symbol(fixed, Decl(sharedMemberLayoutTupleBases.ts, 7, 13)) + + diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types new file mode 100644 index 0000000000000..70b97d1b028aa --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types @@ -0,0 +1,38 @@ +//// [tests/cases/compiler/sharedMemberLayoutTupleBases.ts] //// + +=== sharedMemberLayoutTupleBases.ts === +// Each tuple base can have a different target, and so a different member layout. + +type Prefixed = [number, ...T]; +>Prefixed : [number, ...T] + +interface FromTuple extends Prefixed {} +declare const generic: FromTuple; +>generic : FromTuple + +const genericFirst = generic[0]; +>genericFirst : number +>generic[0] : number +>generic : FromTuple +>0 : 0 + +const genericAll = { ...generic }; +>genericAll : { [n: number]: unknown; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): unknown; push(...items: unknown[]): number; concat(...items: ConcatArray[]): unknown[]; concat(...items: unknown[]): unknown[]; join(separator?: string): string; reverse(): unknown[]; shift(): unknown; slice(start?: number, end?: number): unknown[]; sort(compareFn?: ((a: unknown, b: unknown) => number) | undefined): FromTuple; splice(start: number, deleteCount?: number): unknown[]; splice(start: number, deleteCount: number, ...items: unknown[]): unknown[]; unshift(...items: unknown[]): number; indexOf(searchElement: unknown, fromIndex?: number): number; lastIndexOf(searchElement: unknown, fromIndex?: number): number; every(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): boolean; some(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: unknown, index: number, array: unknown[]) => void, thisArg?: any): void; map(callbackfn: (value: unknown, index: number, array: unknown[]) => U, thisArg?: any): U[]; filter(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): unknown[]; reduce(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown): unknown; reduce(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown; reduce(callbackfn: (previousValue: U, currentValue: unknown, currentIndex: number, array: unknown[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown): unknown; reduceRight(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown; reduceRight(callbackfn: (previousValue: U, currentValue: unknown, currentIndex: number, array: unknown[]) => U, initialValue: U): U; find(predicate: (value: unknown, index: number, obj: unknown[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: unknown, index: number, obj: unknown[]) => unknown, thisArg?: any): unknown; findIndex(predicate: (value: unknown, index: number, obj: unknown[]) => unknown, thisArg?: any): number; fill(value: unknown, start?: number, end?: number): FromTuple; copyWithin(target: number, start: number, end?: number): FromTuple; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, unknown]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: unknown, fromIndex?: number): boolean; flatMap(callback: (this: This, value: unknown, index: number, array: unknown[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): unknown; findLast(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): unknown; findLastIndex(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): number; toReversed(): unknown[]; toSorted(compareFn?: ((a: unknown, b: unknown) => number) | undefined): unknown[]; toSpliced(start: number, deleteCount: number, ...items: unknown[]): unknown[]; toSpliced(start: number, deleteCount?: number): unknown[]; with(index: number, value: unknown): unknown[]; 0: number; length: number; } +>{ ...generic } : { [n: number]: unknown; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): unknown; push(...items: unknown[]): number; concat(...items: ConcatArray[]): unknown[]; concat(...items: unknown[]): unknown[]; join(separator?: string): string; reverse(): unknown[]; shift(): unknown; slice(start?: number, end?: number): unknown[]; sort(compareFn?: ((a: unknown, b: unknown) => number) | undefined): FromTuple; splice(start: number, deleteCount?: number): unknown[]; splice(start: number, deleteCount: number, ...items: unknown[]): unknown[]; unshift(...items: unknown[]): number; indexOf(searchElement: unknown, fromIndex?: number): number; lastIndexOf(searchElement: unknown, fromIndex?: number): number; every(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): boolean; some(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: unknown, index: number, array: unknown[]) => void, thisArg?: any): void; map(callbackfn: (value: unknown, index: number, array: unknown[]) => U, thisArg?: any): U[]; filter(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): unknown[]; reduce(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown): unknown; reduce(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown; reduce(callbackfn: (previousValue: U, currentValue: unknown, currentIndex: number, array: unknown[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown): unknown; reduceRight(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown; reduceRight(callbackfn: (previousValue: U, currentValue: unknown, currentIndex: number, array: unknown[]) => U, initialValue: U): U; find(predicate: (value: unknown, index: number, obj: unknown[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: unknown, index: number, obj: unknown[]) => unknown, thisArg?: any): unknown; findIndex(predicate: (value: unknown, index: number, obj: unknown[]) => unknown, thisArg?: any): number; fill(value: unknown, start?: number, end?: number): FromTuple; copyWithin(target: number, start: number, end?: number): FromTuple; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, unknown]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: unknown, fromIndex?: number): boolean; flatMap(callback: (this: This, value: unknown, index: number, array: unknown[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): unknown; findLast(predicate: (value: unknown, index: number, array: unknown[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): unknown; findLastIndex(predicate: (value: unknown, index: number, array: unknown[]) => unknown, thisArg?: any): number; toReversed(): unknown[]; toSorted(compareFn?: ((a: unknown, b: unknown) => number) | undefined): unknown[]; toSpliced(start: number, deleteCount: number, ...items: unknown[]): unknown[]; toSpliced(start: number, deleteCount?: number): unknown[]; with(index: number, value: unknown): unknown[]; 0: number; length: number; } +>generic : FromTuple + +declare const fixed: FromTuple<[string]>; +>fixed : FromTuple<[string]> + +const fixedSecond = fixed[1]; +>fixedSecond : string | number +>fixed[1] : string | number +>fixed : FromTuple<[string]> +>1 : 1 + +const fixedAll = { ...fixed }; +>fixedAll : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; } +>{ ...fixed } : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; } +>fixed : FromTuple<[string]> + + diff --git a/tsc/testdata/tests/cases/compiler/sharedMemberLayoutTupleBases.ts b/tsc/testdata/tests/cases/compiler/sharedMemberLayoutTupleBases.ts new file mode 100644 index 0000000000000..6bfee2e00f1c4 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/sharedMemberLayoutTupleBases.ts @@ -0,0 +1,14 @@ +// @strict: true +// @noEmit: true + +// Each tuple base can have a different target, and so a different member layout. + +type Prefixed = [number, ...T]; +interface FromTuple extends Prefixed {} +declare const generic: FromTuple; +const genericFirst = generic[0]; +const genericAll = { ...generic }; +declare const fixed: FromTuple<[string]>; +const fixedSecond = fixed[1]; +const fixedAll = { ...fixed }; + From 1988b23b5e9130817841e4e0be9173fc9d1f48f0 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:14:38 -0700 Subject: [PATCH 12/28] Reuse member layouts only with the same base layouts Inherited member slots index into the base's layout, so a shared layout is only valid for instantiations whose bases resolve to the exact base layouts it was built from. Instantiations of a variadic tuple base produce different tuple targets with distinct layouts, so reusing the cached layout read members from the wrong slots. --- tsc/internal/checker/checker.go | 36 ++++++++++++++----- .../sharedMemberLayoutTupleBases.symbols | 1 + .../sharedMemberLayoutTupleBases.types | 8 ++--- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 793578b4dfcb4..17db8c38f9b7a 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19623,6 +19623,12 @@ type memberLayout struct { index []uint32 // Open-addressed hash table of 1-based member indices, or nil for small layouts propertyCount int // The properties are the first members, in property order shared bool + bases []memberLayoutBase // The bases the layout was built from, which a shared layout's instantiations must match +} + +type memberLayoutBase struct { + layout *memberLayout // Layout of a base type with one, through which inherited members are found + isAny bool } type memberOrigin struct { @@ -19705,29 +19711,35 @@ func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { } return layout } - if !c.canShareMemberLayout(source, bases) { + if !c.canShareMemberLayout(d.memberLayout, source, bases) { return c.buildMemberLayout(source, bases) } return d.memberLayout } -// Reports whether the bases of an instantiation have the members the shared layout expects. A base -// type instantiation that hit an instantiation limit is the error type, which has no members, and -// a base whose own base hit the limit has a smaller layout that isn't shared. -func (c *Checker) canShareMemberLayout(source *Type, bases []*Type) bool { +// Reports whether the bases of an instantiation have the layouts the shared layout was built from, since +// inherited members are found through their slots in those layouts. Instantiations of a variadic tuple +// base can have different tuple targets, a base type instantiation that hit an instantiation limit is +// the error type, and a base whose own base hit the limit has a smaller layout that isn't shared. +func (c *Checker) canShareMemberLayout(layout *memberLayout, source *Type, bases []*Type) bool { declaredBases := c.getBaseTypes(source) for k, base := range bases { - if base == c.errorType { - if declaredBases[k] != c.errorType { + expected := layout.bases[k] + if base == c.anyType || expected.isAny { + if base != c.anyType || !expected.isAny { return false } continue } + var baseLayout *memberLayout if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { - if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil && !lazy.layout.shared { - return false + if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { + baseLayout = lazy.layout } } + if baseLayout != expected.layout || base == c.errorType && declaredBases[k] != c.errorType { + return false + } } return true } @@ -19764,14 +19776,17 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { // Resolve the bases first, since this may build other layouts with the scratch storage below. baseMembersList := make([]baseMembers, len(bases)) capacity := len(d.declaredMembers) + layoutBases := make([]memberLayoutBase, len(bases)) for k, base := range bases { if base == c.anyType { + layoutBases[k].isAny = true continue } if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { shared = shared && lazy.layout.shared baseMembersList[k] = baseMembers{layout: lazy.layout, kind: memberOriginBaseLayout} + layoutBases[k].layout = lazy.layout capacity += lazy.layout.propertyCount continue } @@ -19850,6 +19865,9 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { propertyCount: propertyCount, shared: shared, } + if shared { + layout.bases = layoutBases + } for i, e := range entries { layout.symbols[i] = e.symbol layout.origins[i] = e.origin diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols index 2333b1b223504..663335099427d 100644 --- a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.symbols @@ -34,6 +34,7 @@ declare const fixed: FromTuple<[string]>; const fixedSecond = fixed[1]; >fixedSecond : Symbol(fixedSecond, Decl(sharedMemberLayoutTupleBases.ts, 8, 5)) >fixed : Symbol(fixed, Decl(sharedMemberLayoutTupleBases.ts, 7, 13)) +>1 : Symbol(1) const fixedAll = { ...fixed }; >fixedAll : Symbol(fixedAll, Decl(sharedMemberLayoutTupleBases.ts, 9, 5)) diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types index 70b97d1b028aa..3e170e1ab7d6e 100644 --- a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutTupleBases.types @@ -25,14 +25,14 @@ declare const fixed: FromTuple<[string]>; >fixed : FromTuple<[string]> const fixedSecond = fixed[1]; ->fixedSecond : string | number ->fixed[1] : string | number +>fixedSecond : string +>fixed[1] : string >fixed : FromTuple<[string]> >1 : 1 const fixedAll = { ...fixed }; ->fixedAll : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; } ->{ ...fixed } : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; } +>fixedAll : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; length: 2; } +>{ ...fixed } : { [n: number]: string | number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): string | number | undefined; push(...items: (string | number)[]): number; concat(...items: ConcatArray[]): (string | number)[]; concat(...items: (string | number | ConcatArray)[]): (string | number)[]; join(separator?: string): string; reverse(): (string | number)[]; shift(): string | number | undefined; slice(start?: number, end?: number): (string | number)[]; sort(compareFn?: ((a: string | number, b: string | number) => number) | undefined): FromTuple<[string]>; splice(start: number, deleteCount?: number): (string | number)[]; splice(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; unshift(...items: (string | number)[]): number; indexOf(searchElement: string | number, fromIndex?: number): number; lastIndexOf(searchElement: string | number, fromIndex?: number): number; every(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: string | number, index: number, array: (string | number)[]) => void, thisArg?: any): void; map(callbackfn: (value: string | number, index: number, array: (string | number)[]) => U, thisArg?: any): U[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduce(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduce(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number): string | number; reduceRight(callbackfn: (previousValue: string | number, currentValue: string | number, currentIndex: number, array: (string | number)[]) => string | number, initialValue: string | number): string | number; reduceRight(callbackfn: (previousValue: U, currentValue: string | number, currentIndex: number, array: (string | number)[]) => U, initialValue: U): U; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findIndex(predicate: (value: string | number, index: number, obj: (string | number)[]) => unknown, thisArg?: any): number; fill(value: string | number, start?: number, end?: number): FromTuple<[string]>; copyWithin(target: number, start: number, end?: number): FromTuple<[string]>; [Symbol.iterator](): ArrayIterator; entries(): ArrayIterator<[number, string | number]>; keys(): ArrayIterator; values(): ArrayIterator; [Symbol.unscopables]: { [x: number]: boolean | undefined; length?: boolean | undefined; toString?: boolean | undefined; toLocaleString?: boolean | undefined; pop?: boolean | undefined; push?: boolean | undefined; concat?: boolean | undefined; join?: boolean | undefined; reverse?: boolean | undefined; shift?: boolean | undefined; slice?: boolean | undefined; sort?: boolean | undefined; splice?: boolean | undefined; unshift?: boolean | undefined; indexOf?: boolean | undefined; lastIndexOf?: boolean | undefined; every?: boolean | undefined; some?: boolean | undefined; forEach?: boolean | undefined; map?: boolean | undefined; filter?: boolean | undefined; reduce?: boolean | undefined; reduceRight?: boolean | undefined; find?: boolean | undefined; findIndex?: boolean | undefined; fill?: boolean | undefined; copyWithin?: boolean | undefined; [Symbol.iterator]?: boolean | undefined; entries?: boolean | undefined; keys?: boolean | undefined; values?: boolean | undefined; readonly [Symbol.unscopables]?: boolean | undefined; includes?: boolean | undefined; flatMap?: boolean | undefined; flat?: boolean | undefined; at?: boolean | undefined; findLast?: boolean | undefined; findLastIndex?: boolean | undefined; toReversed?: boolean | undefined; toSorted?: boolean | undefined; toSpliced?: boolean | undefined; with?: boolean | undefined; }; includes(searchElement: string | number, fromIndex?: number): boolean; flatMap(callback: (this: This, value: string | number, index: number, array: (string | number)[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; at(index: number): string | number | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => value is S, thisArg?: any): S | undefined; findLast(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): string | number | undefined; findLastIndex(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): number; toReversed(): (string | number)[]; toSorted(compareFn?: ((a: string | number, b: string | number) => number) | undefined): (string | number)[]; toSpliced(start: number, deleteCount: number, ...items: (string | number)[]): (string | number)[]; toSpliced(start: number, deleteCount?: number): (string | number)[]; with(index: number, value: string | number): (string | number)[]; 0: number; 1: string; length: 2; } >fixed : FromTuple<[string]> From c82b73e5b75a374d0528be147b737975eb572ae9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:02:12 -0700 Subject: [PATCH 13/28] Test member layouts of bases with this-dependent intersections An intersection base without type parameters can still reference the polymorphic this type, so its members can differ between instantiations of the derived type. Shared member layouts assumed otherwise and crash looking up a member that no longer exists. --- .../sharedMemberLayoutThisIntersectionBase.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tsc/testdata/tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts diff --git a/tsc/testdata/tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts b/tsc/testdata/tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts new file mode 100644 index 0000000000000..71209527ff35b --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts @@ -0,0 +1,17 @@ +// @strict: true +// @noEmit: true + +interface A { kind: string; m: this["kind"] } +interface B { m: "a" } +type AB = A & B; + +interface D extends AB { kind: T } +declare let d: D<"b">; +d.m; + +declare const ABCtor: new () => AB; +class C extends ABCtor { declare kind: T } +declare let c: C<"b">; +c.m; + +export {}; From 2ffc24b8b5af5320de1815043f9f66d6d87f557d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:02:12 -0700 Subject: [PATCH 14/28] Test member layouts of types with self-referential late-bound names A computed member name that reads a member of its own class or interface resolves the type's members while its late-bound members are still being bound. The layout built from that partial member table must not be reused once the late-bound members are known. --- ...lateBoundMemberLayoutReentrancy.errors.txt | 44 ++++++++++ .../lateBoundMemberLayoutReentrancy.js | 52 +++++++++++ .../lateBoundMemberLayoutReentrancy.symbols | 87 +++++++++++++++++++ .../lateBoundMemberLayoutReentrancy.types | 86 ++++++++++++++++++ .../lateBoundMemberLayoutReentrancy.ts | 26 ++++++ 5 files changed, 295 insertions(+) create mode 100644 tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js create mode 100644 tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types create mode 100644 tsc/testdata/tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt new file mode 100644 index 0000000000000..61fcd1c4914aa --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt @@ -0,0 +1,44 @@ +lateBoundMemberLayoutReentrancy.ts(8,23): error TS2339: Property 'a' does not exist on type 'RI<"b">'. +lateBoundMemberLayoutReentrancy.ts(9,22): error TS2339: Property 'a' does not exist on type 'RI<"a">'. +lateBoundMemberLayoutReentrancy.ts(11,14): error TS2322: Type '"a"' is not assignable to type '"x"'. +lateBoundMemberLayoutReentrancy.ts(12,37): error TS2353: Object literal may only specify known properties, and 'a' does not exist in type 'RI<"b">'. +lateBoundMemberLayoutReentrancy.ts(21,23): error TS2339: Property 'a' does not exist on type 'RC<"b">'. +lateBoundMemberLayoutReentrancy.ts(22,22): error TS2339: Property 'a' does not exist on type 'RC<"a">'. + + +==== lateBoundMemberLayoutReentrancy.ts (6 errors) ==== + declare const rI: RI<"a">; + rI.x + interface RI { + x: T; + [rI.x]: "b"; + } + declare const rI2: RI<"b">; + export const v1 = rI2.a; + ~ +!!! error TS2339: Property 'a' does not exist on type 'RI<"b">'. + export const v2 = rI.a; + ~ +!!! error TS2339: Property 'a' does not exist on type 'RI<"a">'. + export type K2 = keyof RI<"b">; + export const k: K2 = "a"; + ~ +!!! error TS2322: Type '"a"' is not assignable to type '"x"'. + export const o: RI<"b"> = { x: "b", a: "b" }; + ~ +!!! error TS2353: Object literal may only specify known properties, and 'a' does not exist in type 'RI<"b">'. + + declare const rC: RC<"a">; + rC.x + declare class RC { + x: T; + [rC.x]: "b"; + } + declare const rC2: RC<"b">; + export const w1 = rC2.a; + ~ +!!! error TS2339: Property 'a' does not exist on type 'RC<"b">'. + export const w2 = rC.a; + ~ +!!! error TS2339: Property 'a' does not exist on type 'RC<"a">'. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js new file mode 100644 index 0000000000000..74f055cc0d76d --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js @@ -0,0 +1,52 @@ +//// [tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts] //// + +//// [lateBoundMemberLayoutReentrancy.ts] +declare const rI: RI<"a">; +rI.x +interface RI { + x: T; + [rI.x]: "b"; +} +declare const rI2: RI<"b">; +export const v1 = rI2.a; +export const v2 = rI.a; +export type K2 = keyof RI<"b">; +export const k: K2 = "a"; +export const o: RI<"b"> = { x: "b", a: "b" }; + +declare const rC: RC<"a">; +rC.x +declare class RC { + x: T; + [rC.x]: "b"; +} +declare const rC2: RC<"b">; +export const w1 = rC2.a; +export const w2 = rC.a; + + +//// [lateBoundMemberLayoutReentrancy.js] +rI.x; +export const v1 = rI2.a; +export const v2 = rI.a; +export const k = "a"; +export const o = { x: "b", a: "b" }; +rC.x; +export const w1 = rC2.a; +export const w2 = rC.a; + + +//// [lateBoundMemberLayoutReentrancy.d.ts] +declare const rI: RI<"a">; +interface RI { + x: T; + [rI.x]: "b"; +} +export declare const v1: any; +export declare const v2: any; +export type K2 = keyof RI<"b">; +export declare const k: K2; +export declare const o: RI<"b">; +export declare const w1: any; +export declare const w2: any; +export {}; diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols new file mode 100644 index 0000000000000..b8564b020c6d4 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols @@ -0,0 +1,87 @@ +//// [tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts] //// + +=== lateBoundMemberLayoutReentrancy.ts === +declare const rI: RI<"a">; +>rI : Symbol(rI, Decl(lateBoundMemberLayoutReentrancy.ts, 0, 13)) +>RI : Symbol(RI, Decl(lateBoundMemberLayoutReentrancy.ts, 1, 4)) + +rI.x +>rI.x : Symbol(RI.x, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 35)) +>rI : Symbol(rI, Decl(lateBoundMemberLayoutReentrancy.ts, 0, 13)) +>x : Symbol(RI.x, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 35)) + +interface RI { +>RI : Symbol(RI, Decl(lateBoundMemberLayoutReentrancy.ts, 1, 4)) +>T : Symbol(T, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 13)) + + x: T; +>x : Symbol(RI.x, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 35)) +>T : Symbol(T, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 13)) + + [rI.x]: "b"; +>[rI.x] : Symbol(RI[rI.x], Decl(lateBoundMemberLayoutReentrancy.ts, 3, 9)) +>rI.x : Symbol(RI.x, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 35)) +>rI : Symbol(rI, Decl(lateBoundMemberLayoutReentrancy.ts, 0, 13)) +>x : Symbol(RI.x, Decl(lateBoundMemberLayoutReentrancy.ts, 2, 35)) +} +declare const rI2: RI<"b">; +>rI2 : Symbol(rI2, Decl(lateBoundMemberLayoutReentrancy.ts, 6, 13)) +>RI : Symbol(RI, Decl(lateBoundMemberLayoutReentrancy.ts, 1, 4)) + +export const v1 = rI2.a; +>v1 : Symbol(v1, Decl(lateBoundMemberLayoutReentrancy.ts, 7, 12)) +>rI2 : Symbol(rI2, Decl(lateBoundMemberLayoutReentrancy.ts, 6, 13)) + +export const v2 = rI.a; +>v2 : Symbol(v2, Decl(lateBoundMemberLayoutReentrancy.ts, 8, 12)) +>rI : Symbol(rI, Decl(lateBoundMemberLayoutReentrancy.ts, 0, 13)) + +export type K2 = keyof RI<"b">; +>K2 : Symbol(K2, Decl(lateBoundMemberLayoutReentrancy.ts, 8, 23)) +>RI : Symbol(RI, Decl(lateBoundMemberLayoutReentrancy.ts, 1, 4)) + +export const k: K2 = "a"; +>k : Symbol(k, Decl(lateBoundMemberLayoutReentrancy.ts, 10, 12)) +>K2 : Symbol(K2, Decl(lateBoundMemberLayoutReentrancy.ts, 8, 23)) + +export const o: RI<"b"> = { x: "b", a: "b" }; +>o : Symbol(o, Decl(lateBoundMemberLayoutReentrancy.ts, 11, 12)) +>RI : Symbol(RI, Decl(lateBoundMemberLayoutReentrancy.ts, 1, 4)) +>x : Symbol(x, Decl(lateBoundMemberLayoutReentrancy.ts, 11, 27)) +>a : Symbol(a, Decl(lateBoundMemberLayoutReentrancy.ts, 11, 35)) + +declare const rC: RC<"a">; +>rC : Symbol(rC, Decl(lateBoundMemberLayoutReentrancy.ts, 13, 13)) +>RC : Symbol(RC, Decl(lateBoundMemberLayoutReentrancy.ts, 14, 4)) + +rC.x +>rC.x : Symbol(RC.x, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 39)) +>rC : Symbol(rC, Decl(lateBoundMemberLayoutReentrancy.ts, 13, 13)) +>x : Symbol(RC.x, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 39)) + +declare class RC { +>RC : Symbol(RC, Decl(lateBoundMemberLayoutReentrancy.ts, 14, 4)) +>T : Symbol(T, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 17)) + + x: T; +>x : Symbol(RC.x, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 39)) +>T : Symbol(T, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 17)) + + [rC.x]: "b"; +>[rC.x] : Symbol(RC[rC.x], Decl(lateBoundMemberLayoutReentrancy.ts, 16, 9)) +>rC.x : Symbol(RC.x, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 39)) +>rC : Symbol(rC, Decl(lateBoundMemberLayoutReentrancy.ts, 13, 13)) +>x : Symbol(RC.x, Decl(lateBoundMemberLayoutReentrancy.ts, 15, 39)) +} +declare const rC2: RC<"b">; +>rC2 : Symbol(rC2, Decl(lateBoundMemberLayoutReentrancy.ts, 19, 13)) +>RC : Symbol(RC, Decl(lateBoundMemberLayoutReentrancy.ts, 14, 4)) + +export const w1 = rC2.a; +>w1 : Symbol(w1, Decl(lateBoundMemberLayoutReentrancy.ts, 20, 12)) +>rC2 : Symbol(rC2, Decl(lateBoundMemberLayoutReentrancy.ts, 19, 13)) + +export const w2 = rC.a; +>w2 : Symbol(w2, Decl(lateBoundMemberLayoutReentrancy.ts, 21, 12)) +>rC : Symbol(rC, Decl(lateBoundMemberLayoutReentrancy.ts, 13, 13)) + diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types new file mode 100644 index 0000000000000..0e2ddc2e193eb --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types @@ -0,0 +1,86 @@ +//// [tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts] //// + +=== lateBoundMemberLayoutReentrancy.ts === +declare const rI: RI<"a">; +>rI : RI<"a"> + +rI.x +>rI.x : "a" +>rI : RI<"a"> +>x : "a" + +interface RI { + x: T; +>x : T + + [rI.x]: "b"; +>[rI.x] : "b" +>rI.x : "a" +>rI : RI<"a"> +>x : "a" +} +declare const rI2: RI<"b">; +>rI2 : RI<"b"> + +export const v1 = rI2.a; +>v1 : any +>rI2.a : any +>rI2 : RI<"b"> +>a : any + +export const v2 = rI.a; +>v2 : any +>rI.a : any +>rI : RI<"a"> +>a : any + +export type K2 = keyof RI<"b">; +>K2 : "x" + +export const k: K2 = "a"; +>k : "x" +>"a" : "a" + +export const o: RI<"b"> = { x: "b", a: "b" }; +>o : RI<"b"> +>{ x: "b", a: "b" } : { x: "b"; a: string; } +>x : "b" +>"b" : "b" +>a : string +>"b" : "b" + +declare const rC: RC<"a">; +>rC : RC<"a"> + +rC.x +>rC.x : "a" +>rC : RC<"a"> +>x : "a" + +declare class RC { +>RC : RC + + x: T; +>x : T + + [rC.x]: "b"; +>[rC.x] : "b" +>rC.x : "a" +>rC : RC<"a"> +>x : "a" +} +declare const rC2: RC<"b">; +>rC2 : RC<"b"> + +export const w1 = rC2.a; +>w1 : any +>rC2.a : any +>rC2 : RC<"b"> +>a : any + +export const w2 = rC.a; +>w2 : any +>rC.a : any +>rC : RC<"a"> +>a : any + diff --git a/tsc/testdata/tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts b/tsc/testdata/tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts new file mode 100644 index 0000000000000..e998bc6d2c356 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/lateBoundMemberLayoutReentrancy.ts @@ -0,0 +1,26 @@ +// @target: esnext +// @strict: true +// @declaration: true + +declare const rI: RI<"a">; +rI.x +interface RI { + x: T; + [rI.x]: "b"; +} +declare const rI2: RI<"b">; +export const v1 = rI2.a; +export const v2 = rI.a; +export type K2 = keyof RI<"b">; +export const k: K2 = "a"; +export const o: RI<"b"> = { x: "b", a: "b" }; + +declare const rC: RC<"a">; +rC.x +declare class RC { + x: T; + [rC.x]: "b"; +} +declare const rC2: RC<"b">; +export const w1 = rC2.a; +export const w2 = rC.a; From f4227dc7bfd4fe57d7ec0b8cf3049d53963ce788 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:04:00 -0700 Subject: [PATCH 15/28] Share member layouts only when every base has a layout Finding inherited members by name relied on bases without type parameters having the same members in every instantiation, which doesn't hold when a base references the polymorphic this type. A layout is now shared only when each base's members are known to be fixed: the base is any or has a shared layout itself. --- tsc/internal/checker/checker.go | 51 ++++--------------- ...emberLayoutThisIntersectionBase.errors.txt | 36 +++++++++++++ ...edMemberLayoutThisIntersectionBase.symbols | 51 +++++++++++++++++++ ...aredMemberLayoutThisIntersectionBase.types | 42 +++++++++++++++ 4 files changed, 140 insertions(+), 40 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.types diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 17db8c38f9b7a..cccf1149b2059 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19614,8 +19614,10 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters // A memberLayout describes the members of a class or interface type and of its instantiations: // their keys, their order, and where each one comes from. Member keys and order don't depend on -// type arguments when every base type has a shared layout, in which case the layout is computed -// once per class or interface and shared by every instantiation. +// type arguments when every base type has a shared layout or is any, in which case the layout is +// computed once per class or interface and shared by every instantiation. Any other base type, +// even one without type parameters, can have different members in each instantiation, for example +// an intersection that references the polymorphic this type and reduces to never. type memberLayout struct { symbols []*ast.Symbol // Declared or inherited symbols, with the names and flags of every instantiation's members keys []string // Member keys, or nil when every key is its symbol's name @@ -19642,7 +19644,6 @@ type memberOriginKind uint8 const ( memberOriginDeclared memberOriginKind = iota // Declared member, instantiated with the type's mapper memberOriginBaseLayout // Member of the base type, through the base type's layout - memberOriginBaseName // Property of the base type with the same name memberOriginFixed // The symbol itself, in a layout that isn't shared ) @@ -19711,7 +19712,7 @@ func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { } return layout } - if !c.canShareMemberLayout(d.memberLayout, source, bases) { + if !c.canShareMemberLayout(d.memberLayout, bases) { return c.buildMemberLayout(source, bases) } return d.memberLayout @@ -19721,8 +19722,7 @@ func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { // inherited members are found through their slots in those layouts. Instantiations of a variadic tuple // base can have different tuple targets, a base type instantiation that hit an instantiation limit is // the error type, and a base whose own base hit the limit has a smaller layout that isn't shared. -func (c *Checker) canShareMemberLayout(layout *memberLayout, source *Type, bases []*Type) bool { - declaredBases := c.getBaseTypes(source) +func (c *Checker) canShareMemberLayout(layout *memberLayout, bases []*Type) bool { for k, base := range bases { expected := layout.bases[k] if base == c.anyType || expected.isAny { @@ -19731,13 +19731,10 @@ func (c *Checker) canShareMemberLayout(layout *memberLayout, source *Type, bases } continue } - var baseLayout *memberLayout - if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { - if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { - baseLayout = lazy.layout - } + if base.flags&TypeFlagsObject == 0 || base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) == 0 { + return false } - if baseLayout != expected.layout || base == c.errorType && declaredBases[k] != c.errorType { + if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy == nil || lazy.layout != expected.layout { return false } } @@ -19769,10 +19766,6 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { d := source.AsInterfaceType() // A layout computed while base types are still being resolved may be missing inherited members. shared := d.baseTypesResolved - var declaredBases []*Type - if shared { - declaredBases = c.getBaseTypes(source) - } // Resolve the bases first, since this may build other layouts with the scratch storage below. baseMembersList := make([]baseMembers, len(bases)) capacity := len(d.declaredMembers) @@ -19791,15 +19784,8 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { continue } } - // The properties of a base type without a layout have the same names in every instantiation - // when the declared base type references no type parameters, so they can be found by name. - kind := memberOriginFixed - if shared && k < len(declaredBases) && !c.couldContainTypeVariables(declaredBases[k]) { - kind = memberOriginBaseName - } else { - shared = false - } - baseMembersList[k] = baseMembers{properties: c.getPropertiesOfType(base), kind: kind} + shared = false + baseMembersList[k] = baseMembers{properties: c.getPropertiesOfType(base), kind: memberOriginFixed} capacity += len(baseMembersList[k].properties) } // Reuse scratch storage, since this runs for every class and interface. @@ -19912,26 +19898,11 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { base := c.resolveStructuredTypeMembers(l.bases[origin.base]).lazy debug.Assert(base != nil, "Base type must have lazy members") symbol = c.getLazyMember(base, int(origin.slot)) - case memberOriginBaseName: - symbol = c.getBasePropertyByName(l.bases[origin.base], l.layout.key(i)) } l.slots[i] = symbol return symbol } -// Returns the property of a base type with the given name, as found in getPropertiesOfType. -func (c *Checker) getBasePropertyByName(base *Type, name string) *ast.Symbol { - base = c.getReducedApparentType(base) - var symbol *ast.Symbol - if base.flags&TypeFlagsUnionOrIntersection != 0 { - symbol = c.getPropertyOfUnionOrIntersectionType(base, name, base.flags&TypeFlagsIntersection != 0 /*skipObjectFunctionPropertyAugment*/) - } else if base.flags&TypeFlagsObject != 0 { - symbol = c.resolveStructuredTypeMembers(base).getMember(name) - } - debug.Assert(symbol != nil, "Base type must have property") - return symbol -} - // Returns the number of members, which for instantiations are only the properties (see instantiateSymbolTable). func (l *lazyMembers) memberCount() int { if l.mapper != nil { diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.errors.txt b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.errors.txt new file mode 100644 index 0000000000000..38ea8e221e2e3 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.errors.txt @@ -0,0 +1,36 @@ +sharedMemberLayoutThisIntersectionBase.ts(5,11): error TS2589: Type instantiation is excessively deep and possibly infinite. +sharedMemberLayoutThisIntersectionBase.ts(7,1): error TS2589: Type instantiation is excessively deep and possibly infinite. +sharedMemberLayoutThisIntersectionBase.ts(7,3): error TS2339: Property 'm' does not exist on type 'D<"b">'. +sharedMemberLayoutThisIntersectionBase.ts(10,7): error TS2589: Type instantiation is excessively deep and possibly infinite. +sharedMemberLayoutThisIntersectionBase.ts(12,1): error TS2589: Type instantiation is excessively deep and possibly infinite. +sharedMemberLayoutThisIntersectionBase.ts(12,3): error TS2339: Property 'm' does not exist on type 'C<"b">'. + + +==== sharedMemberLayoutThisIntersectionBase.ts (6 errors) ==== + interface A { kind: string; m: this["kind"] } + interface B { m: "a" } + type AB = A & B; + + interface D extends AB { kind: T } + ~ +!!! error TS2589: Type instantiation is excessively deep and possibly infinite. + declare let d: D<"b">; + d.m; + ~~~ +!!! error TS2589: Type instantiation is excessively deep and possibly infinite. + ~ +!!! error TS2339: Property 'm' does not exist on type 'D<"b">'. + + declare const ABCtor: new () => AB; + class C extends ABCtor { declare kind: T } + ~ +!!! error TS2589: Type instantiation is excessively deep and possibly infinite. + declare let c: C<"b">; + c.m; + ~~~ +!!! error TS2589: Type instantiation is excessively deep and possibly infinite. + ~ +!!! error TS2339: Property 'm' does not exist on type 'C<"b">'. + + export {}; + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.symbols b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.symbols new file mode 100644 index 0000000000000..793b38f82728b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.symbols @@ -0,0 +1,51 @@ +//// [tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts] //// + +=== sharedMemberLayoutThisIntersectionBase.ts === +interface A { kind: string; m: this["kind"] } +>A : Symbol(A, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 0)) +>kind : Symbol(A.kind, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 13)) +>m : Symbol(A.m, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 27)) + +interface B { m: "a" } +>B : Symbol(B, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 45)) +>m : Symbol(B.m, Decl(sharedMemberLayoutThisIntersectionBase.ts, 1, 13)) + +type AB = A & B; +>AB : Symbol(AB, Decl(sharedMemberLayoutThisIntersectionBase.ts, 1, 22)) +>A : Symbol(A, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 0)) +>B : Symbol(B, Decl(sharedMemberLayoutThisIntersectionBase.ts, 0, 45)) + +interface D extends AB { kind: T } +>D : Symbol(D, Decl(sharedMemberLayoutThisIntersectionBase.ts, 2, 16)) +>T : Symbol(T, Decl(sharedMemberLayoutThisIntersectionBase.ts, 4, 12)) +>AB : Symbol(AB, Decl(sharedMemberLayoutThisIntersectionBase.ts, 1, 22)) +>kind : Symbol(D.kind, Decl(sharedMemberLayoutThisIntersectionBase.ts, 4, 42)) +>T : Symbol(T, Decl(sharedMemberLayoutThisIntersectionBase.ts, 4, 12)) + +declare let d: D<"b">; +>d : Symbol(d, Decl(sharedMemberLayoutThisIntersectionBase.ts, 5, 11)) +>D : Symbol(D, Decl(sharedMemberLayoutThisIntersectionBase.ts, 2, 16)) + +d.m; +>d : Symbol(d, Decl(sharedMemberLayoutThisIntersectionBase.ts, 5, 11)) + +declare const ABCtor: new () => AB; +>ABCtor : Symbol(ABCtor, Decl(sharedMemberLayoutThisIntersectionBase.ts, 8, 13)) +>AB : Symbol(AB, Decl(sharedMemberLayoutThisIntersectionBase.ts, 1, 22)) + +class C extends ABCtor { declare kind: T } +>C : Symbol(C, Decl(sharedMemberLayoutThisIntersectionBase.ts, 8, 35)) +>T : Symbol(T, Decl(sharedMemberLayoutThisIntersectionBase.ts, 9, 8)) +>ABCtor : Symbol(ABCtor, Decl(sharedMemberLayoutThisIntersectionBase.ts, 8, 13)) +>kind : Symbol(C.kind, Decl(sharedMemberLayoutThisIntersectionBase.ts, 9, 42)) +>T : Symbol(T, Decl(sharedMemberLayoutThisIntersectionBase.ts, 9, 8)) + +declare let c: C<"b">; +>c : Symbol(c, Decl(sharedMemberLayoutThisIntersectionBase.ts, 10, 11)) +>C : Symbol(C, Decl(sharedMemberLayoutThisIntersectionBase.ts, 8, 35)) + +c.m; +>c : Symbol(c, Decl(sharedMemberLayoutThisIntersectionBase.ts, 10, 11)) + +export {}; + diff --git a/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.types b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.types new file mode 100644 index 0000000000000..41de65a71ea2f --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/sharedMemberLayoutThisIntersectionBase.types @@ -0,0 +1,42 @@ +//// [tests/cases/compiler/sharedMemberLayoutThisIntersectionBase.ts] //// + +=== sharedMemberLayoutThisIntersectionBase.ts === +interface A { kind: string; m: this["kind"] } +>kind : string +>m : this["kind"] + +interface B { m: "a" } +>m : "a" + +type AB = A & B; +>AB : AB + +interface D extends AB { kind: T } +>kind : T + +declare let d: D<"b">; +>d : D<"b"> + +d.m; +>d.m : any +>d : D<"b"> +>m : any + +declare const ABCtor: new () => AB; +>ABCtor : new () => AB + +class C extends ABCtor { declare kind: T } +>C : C +>ABCtor : A & B +>kind : T + +declare let c: C<"b">; +>c : C<"b"> + +c.m; +>c.m : any +>c : C<"b"> +>m : any + +export {}; + From 81bfcecd62325acd6f13c6e538bec261d9fc3341 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:04:57 -0700 Subject: [PATCH 16/28] Drop member layouts built from partial declared members Late binding a member name can resolve its own type's members before the late-bound members are known, and the shared layout built then was reused for every later instantiation. Dropping it when the declared members are replaced lets the next instantiation build a layout with the complete members. --- tsc/internal/checker/checker.go | 5 +++ ...lateBoundMemberLayoutReentrancy.errors.txt | 44 ------------------- .../lateBoundMemberLayoutReentrancy.js | 8 ++-- .../lateBoundMemberLayoutReentrancy.symbols | 8 ++++ .../lateBoundMemberLayoutReentrancy.types | 32 +++++++------- 5 files changed, 33 insertions(+), 64 deletions(-) delete mode 100644 tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index cccf1149b2059..5e4a4b75fdc22 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -20399,6 +20399,11 @@ func (c *Checker) resolveDeclaredMembers(t *Type) *InterfaceType { d := t.AsInterfaceType() if !d.declaredMembersResolved { members := c.getMembersOfSymbol(t.symbol) + if d.declaredMembersResolved { + // Late binding resolved the declared members again, before its late-bound members were + // known, so a member layout built since then is missing them. + d.memberLayout = nil + } d.declaredMembersResolved = true d.declaredMembers = members d.declaredCallSignatures = c.getSignaturesOfSymbol(d.declaredMembers[ast.InternalSymbolNameCall]) diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt deleted file mode 100644 index 61fcd1c4914aa..0000000000000 --- a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.errors.txt +++ /dev/null @@ -1,44 +0,0 @@ -lateBoundMemberLayoutReentrancy.ts(8,23): error TS2339: Property 'a' does not exist on type 'RI<"b">'. -lateBoundMemberLayoutReentrancy.ts(9,22): error TS2339: Property 'a' does not exist on type 'RI<"a">'. -lateBoundMemberLayoutReentrancy.ts(11,14): error TS2322: Type '"a"' is not assignable to type '"x"'. -lateBoundMemberLayoutReentrancy.ts(12,37): error TS2353: Object literal may only specify known properties, and 'a' does not exist in type 'RI<"b">'. -lateBoundMemberLayoutReentrancy.ts(21,23): error TS2339: Property 'a' does not exist on type 'RC<"b">'. -lateBoundMemberLayoutReentrancy.ts(22,22): error TS2339: Property 'a' does not exist on type 'RC<"a">'. - - -==== lateBoundMemberLayoutReentrancy.ts (6 errors) ==== - declare const rI: RI<"a">; - rI.x - interface RI { - x: T; - [rI.x]: "b"; - } - declare const rI2: RI<"b">; - export const v1 = rI2.a; - ~ -!!! error TS2339: Property 'a' does not exist on type 'RI<"b">'. - export const v2 = rI.a; - ~ -!!! error TS2339: Property 'a' does not exist on type 'RI<"a">'. - export type K2 = keyof RI<"b">; - export const k: K2 = "a"; - ~ -!!! error TS2322: Type '"a"' is not assignable to type '"x"'. - export const o: RI<"b"> = { x: "b", a: "b" }; - ~ -!!! error TS2353: Object literal may only specify known properties, and 'a' does not exist in type 'RI<"b">'. - - declare const rC: RC<"a">; - rC.x - declare class RC { - x: T; - [rC.x]: "b"; - } - declare const rC2: RC<"b">; - export const w1 = rC2.a; - ~ -!!! error TS2339: Property 'a' does not exist on type 'RC<"b">'. - export const w2 = rC.a; - ~ -!!! error TS2339: Property 'a' does not exist on type 'RC<"a">'. - \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js index 74f055cc0d76d..f74a9a2dcb804 100644 --- a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.js @@ -42,11 +42,11 @@ interface RI { x: T; [rI.x]: "b"; } -export declare const v1: any; -export declare const v2: any; +export declare const v1: "b"; +export declare const v2: "b"; export type K2 = keyof RI<"b">; export declare const k: K2; export declare const o: RI<"b">; -export declare const w1: any; -export declare const w2: any; +export declare const w1: "b"; +export declare const w2: "b"; export {}; diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols index b8564b020c6d4..5bdce11469134 100644 --- a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.symbols @@ -30,11 +30,15 @@ declare const rI2: RI<"b">; export const v1 = rI2.a; >v1 : Symbol(v1, Decl(lateBoundMemberLayoutReentrancy.ts, 7, 12)) +>rI2.a : Symbol(RI[rI.x], Decl(lateBoundMemberLayoutReentrancy.ts, 3, 9)) >rI2 : Symbol(rI2, Decl(lateBoundMemberLayoutReentrancy.ts, 6, 13)) +>a : Symbol(RI[rI.x], Decl(lateBoundMemberLayoutReentrancy.ts, 3, 9)) export const v2 = rI.a; >v2 : Symbol(v2, Decl(lateBoundMemberLayoutReentrancy.ts, 8, 12)) +>rI.a : Symbol(RI[rI.x], Decl(lateBoundMemberLayoutReentrancy.ts, 3, 9)) >rI : Symbol(rI, Decl(lateBoundMemberLayoutReentrancy.ts, 0, 13)) +>a : Symbol(RI[rI.x], Decl(lateBoundMemberLayoutReentrancy.ts, 3, 9)) export type K2 = keyof RI<"b">; >K2 : Symbol(K2, Decl(lateBoundMemberLayoutReentrancy.ts, 8, 23)) @@ -79,9 +83,13 @@ declare const rC2: RC<"b">; export const w1 = rC2.a; >w1 : Symbol(w1, Decl(lateBoundMemberLayoutReentrancy.ts, 20, 12)) +>rC2.a : Symbol(RC[rC.x], Decl(lateBoundMemberLayoutReentrancy.ts, 16, 9)) >rC2 : Symbol(rC2, Decl(lateBoundMemberLayoutReentrancy.ts, 19, 13)) +>a : Symbol(RC[rC.x], Decl(lateBoundMemberLayoutReentrancy.ts, 16, 9)) export const w2 = rC.a; >w2 : Symbol(w2, Decl(lateBoundMemberLayoutReentrancy.ts, 21, 12)) +>rC.a : Symbol(RC[rC.x], Decl(lateBoundMemberLayoutReentrancy.ts, 16, 9)) >rC : Symbol(rC, Decl(lateBoundMemberLayoutReentrancy.ts, 13, 13)) +>a : Symbol(RC[rC.x], Decl(lateBoundMemberLayoutReentrancy.ts, 16, 9)) diff --git a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types index 0e2ddc2e193eb..08b081ddf6850 100644 --- a/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types +++ b/tsc/testdata/baselines/reference/compiler/lateBoundMemberLayoutReentrancy.types @@ -23,30 +23,30 @@ declare const rI2: RI<"b">; >rI2 : RI<"b"> export const v1 = rI2.a; ->v1 : any ->rI2.a : any +>v1 : "b" +>rI2.a : "b" >rI2 : RI<"b"> ->a : any +>a : "b" export const v2 = rI.a; ->v2 : any ->rI.a : any +>v2 : "b" +>rI.a : "b" >rI : RI<"a"> ->a : any +>a : "b" export type K2 = keyof RI<"b">; ->K2 : "x" +>K2 : keyof RI<"b"> export const k: K2 = "a"; ->k : "x" +>k : keyof RI<"b"> >"a" : "a" export const o: RI<"b"> = { x: "b", a: "b" }; >o : RI<"b"> ->{ x: "b", a: "b" } : { x: "b"; a: string; } +>{ x: "b", a: "b" } : { x: "b"; a: "b"; } >x : "b" >"b" : "b" ->a : string +>a : "b" >"b" : "b" declare const rC: RC<"a">; @@ -73,14 +73,14 @@ declare const rC2: RC<"b">; >rC2 : RC<"b"> export const w1 = rC2.a; ->w1 : any ->rC2.a : any +>w1 : "b" +>rC2.a : "b" >rC2 : RC<"b"> ->a : any +>a : "b" export const w2 = rC.a; ->w2 : any ->rC.a : any +>w2 : "b" +>rC.a : "b" >rC : RC<"a"> ->a : any +>a : "b" From cfab455b4c8fefe71868d0d569481257f7936774 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:20:59 -0700 Subject: [PATCH 17/28] Verify member layouts against eager member resolution Each review of member layouts has found another way a shared layout can disagree with the members its type would otherwise have. Checking every lazily resolved class and interface type against members resolved as before layouts finds such disagreements across whole test suites and real projects, rather than one case at a time. The check is enabled for now so that test runs over user and top repositories can report mismatches. --- tsc/internal/checker/checker.go | 10 +++ tsc/internal/checker/memberlayoutverify.go | 98 ++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tsc/internal/checker/memberlayoutverify.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 5e4a4b75fdc22..073905a24e6f6 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -602,6 +602,7 @@ type Checker struct { instantiationStack []*Type layoutEntries []layoutEntry layoutIndex map[string]int + memberLayoutVerifications []memberLayoutVerification conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -14232,6 +14233,9 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile c.checkNotCanceled() checkUnused := c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue() || collection == &c.suggestionDiagnostics c.checkSourceFile(ctx, sourceFile, checkUnused) + if verifyMemberLayouts { + c.verifyMemberLayouts() + } if c.wasCanceled { return nil } @@ -19610,6 +19614,9 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters })) } c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) + if verifyMemberLayouts { + c.recordMemberLayoutVerification(t, source) + } } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19897,6 +19904,9 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { case memberOriginBaseLayout: base := c.resolveStructuredTypeMembers(l.bases[origin.base]).lazy debug.Assert(base != nil, "Base type must have lazy members") + if verifyMemberLayouts { + c.verifyMemberLayoutSlot(l, i, base, int(origin.slot)) + } symbol = c.getLazyMember(base, int(origin.slot)) } l.slots[i] = symbol diff --git a/tsc/internal/checker/memberlayoutverify.go b/tsc/internal/checker/memberlayoutverify.go new file mode 100644 index 0000000000000..dc28d53fdfd26 --- /dev/null +++ b/tsc/internal/checker/memberlayoutverify.go @@ -0,0 +1,98 @@ +package checker + +import ( + "fmt" + "maps" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/debug" +) + +// When set, every class or interface type whose members are resolved through a member layout is +// checked against members resolved eagerly from the same declared members, mapper and base types, +// as they were before member layouts. The check runs after each source file is checked, so that +// it doesn't change the order in which types are resolved during checking. +const verifyMemberLayouts = true + +type memberLayoutVerification struct { + t *Type + declaredMembers ast.SymbolTable // The declared members when resolved, which can be partial during late binding + lazy *lazyMembers +} + +func (c *Checker) recordMemberLayoutVerification(t *Type, source *Type) { + c.memberLayoutVerifications = append(c.memberLayoutVerifications, memberLayoutVerification{t: t, declaredMembers: source.AsInterfaceType().declaredMembers, lazy: t.AsStructuredType().lazy}) +} + +func (c *Checker) verifyMemberLayouts() { + // Verification can resolve more types, which records them to be verified too. + for len(c.memberLayoutVerifications) != 0 { + pending := c.memberLayoutVerifications + c.memberLayoutVerifications = nil + for _, v := range pending { + c.verifyMemberLayout(v) + } + } +} + +func (c *Checker) verifyMemberLayout(v memberLayoutVerification) { + fail := func(format string, args ...any) { + debug.Fail(fmt.Sprintf("Member layout of %s doesn't match its eager members: %s", c.TypeToString(v.t), fmt.Sprintf(format, args...))) + } + l := v.lazy + if v.t.AsStructuredType().lazy != l { + // Members resolved again during a circular resolution replace these, and are verified separately. + return + } + var members ast.SymbolTable + if l.mapper == nil { + members = maps.Clone(v.declaredMembers) + } else { + members = c.instantiateSymbolTable(v.declaredMembers, l.mapper) + } + for _, base := range l.bases { + members = c.addInheritedMembers(members, c.getPropertiesOfType(base)) + } + eager := c.getNamedMembers(members, v.t.symbol) + properties := c.getLazyProperties(l) + if len(eager) != len(properties) { + fail("%d properties, expected %d", len(properties), len(eager)) + } + for i, e := range eager { + p := properties[i] + if p.Name() != e.Name() { + fail("property %d is %q, expected %q", i, p.Name(), e.Name()) + } + // Instantiation can return either the original or a new symbol depending on what was resolved + // first, so instantiated symbols are compared by their flags and declarations. + if p != e && !sameMemberSymbol(p, e) { + fail("property %q has a different symbol", e.Name()) + } + } + table := c.getLazyMemberTable(l) + if len(table) != len(members) { + fail("%d members, expected %d", len(table), len(members)) + } + for key, e := range members { + p, ok := table[key] + if !ok { + fail("member %q is missing", key) + } + if p.Name() != e.Name() || !sameMemberSymbol(p, e) { + fail("member %q has a different symbol", key) + } + if c.getLazyMemberByName(l, key) != p { + fail("member %q is found by name as a different symbol", key) + } + } +} + +func sameMemberSymbol(a *ast.Symbol, b *ast.Symbol) bool { + return a.Flags()&^ast.SymbolFlagsTransient == b.Flags()&^ast.SymbolFlagsTransient && a.ValueDeclaration() == b.ValueDeclaration() && len(a.Declarations()) == len(b.Declarations()) +} + +func (c *Checker) verifyMemberLayoutSlot(l *lazyMembers, i int, base *lazyMembers, slot int) { + if slot >= len(base.layout.symbols) || base.layout.key(slot) != l.layout.key(i) { + debug.Fail(fmt.Sprintf("Member layout slot of %q in its base doesn't have the same key", l.layout.key(i))) + } +} From 10716097628affe6f0518ea0c7b5754c1110abc9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:00:44 -0700 Subject: [PATCH 18/28] Revert "Verify member layouts against eager member resolution" The user and top1000 test runs found no mismatches, so the check is no longer needed. This reverts commit cfab455b4c8fefe71868d0d569481257f7936774. --- tsc/internal/checker/checker.go | 10 --- tsc/internal/checker/memberlayoutverify.go | 98 ---------------------- 2 files changed, 108 deletions(-) delete mode 100644 tsc/internal/checker/memberlayoutverify.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 073905a24e6f6..5e4a4b75fdc22 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -602,7 +602,6 @@ type Checker struct { instantiationStack []*Type layoutEntries []layoutEntry layoutIndex map[string]int - memberLayoutVerifications []memberLayoutVerification conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -14233,9 +14232,6 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile c.checkNotCanceled() checkUnused := c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue() || collection == &c.suggestionDiagnostics c.checkSourceFile(ctx, sourceFile, checkUnused) - if verifyMemberLayouts { - c.verifyMemberLayouts() - } if c.wasCanceled { return nil } @@ -19614,9 +19610,6 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters })) } c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) - if verifyMemberLayouts { - c.recordMemberLayoutVerification(t, source) - } } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19904,9 +19897,6 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { case memberOriginBaseLayout: base := c.resolveStructuredTypeMembers(l.bases[origin.base]).lazy debug.Assert(base != nil, "Base type must have lazy members") - if verifyMemberLayouts { - c.verifyMemberLayoutSlot(l, i, base, int(origin.slot)) - } symbol = c.getLazyMember(base, int(origin.slot)) } l.slots[i] = symbol diff --git a/tsc/internal/checker/memberlayoutverify.go b/tsc/internal/checker/memberlayoutverify.go deleted file mode 100644 index dc28d53fdfd26..0000000000000 --- a/tsc/internal/checker/memberlayoutverify.go +++ /dev/null @@ -1,98 +0,0 @@ -package checker - -import ( - "fmt" - "maps" - - "github.com/microsoft/TypeScript/tsc/internal/ast" - "github.com/microsoft/TypeScript/tsc/internal/debug" -) - -// When set, every class or interface type whose members are resolved through a member layout is -// checked against members resolved eagerly from the same declared members, mapper and base types, -// as they were before member layouts. The check runs after each source file is checked, so that -// it doesn't change the order in which types are resolved during checking. -const verifyMemberLayouts = true - -type memberLayoutVerification struct { - t *Type - declaredMembers ast.SymbolTable // The declared members when resolved, which can be partial during late binding - lazy *lazyMembers -} - -func (c *Checker) recordMemberLayoutVerification(t *Type, source *Type) { - c.memberLayoutVerifications = append(c.memberLayoutVerifications, memberLayoutVerification{t: t, declaredMembers: source.AsInterfaceType().declaredMembers, lazy: t.AsStructuredType().lazy}) -} - -func (c *Checker) verifyMemberLayouts() { - // Verification can resolve more types, which records them to be verified too. - for len(c.memberLayoutVerifications) != 0 { - pending := c.memberLayoutVerifications - c.memberLayoutVerifications = nil - for _, v := range pending { - c.verifyMemberLayout(v) - } - } -} - -func (c *Checker) verifyMemberLayout(v memberLayoutVerification) { - fail := func(format string, args ...any) { - debug.Fail(fmt.Sprintf("Member layout of %s doesn't match its eager members: %s", c.TypeToString(v.t), fmt.Sprintf(format, args...))) - } - l := v.lazy - if v.t.AsStructuredType().lazy != l { - // Members resolved again during a circular resolution replace these, and are verified separately. - return - } - var members ast.SymbolTable - if l.mapper == nil { - members = maps.Clone(v.declaredMembers) - } else { - members = c.instantiateSymbolTable(v.declaredMembers, l.mapper) - } - for _, base := range l.bases { - members = c.addInheritedMembers(members, c.getPropertiesOfType(base)) - } - eager := c.getNamedMembers(members, v.t.symbol) - properties := c.getLazyProperties(l) - if len(eager) != len(properties) { - fail("%d properties, expected %d", len(properties), len(eager)) - } - for i, e := range eager { - p := properties[i] - if p.Name() != e.Name() { - fail("property %d is %q, expected %q", i, p.Name(), e.Name()) - } - // Instantiation can return either the original or a new symbol depending on what was resolved - // first, so instantiated symbols are compared by their flags and declarations. - if p != e && !sameMemberSymbol(p, e) { - fail("property %q has a different symbol", e.Name()) - } - } - table := c.getLazyMemberTable(l) - if len(table) != len(members) { - fail("%d members, expected %d", len(table), len(members)) - } - for key, e := range members { - p, ok := table[key] - if !ok { - fail("member %q is missing", key) - } - if p.Name() != e.Name() || !sameMemberSymbol(p, e) { - fail("member %q has a different symbol", key) - } - if c.getLazyMemberByName(l, key) != p { - fail("member %q is found by name as a different symbol", key) - } - } -} - -func sameMemberSymbol(a *ast.Symbol, b *ast.Symbol) bool { - return a.Flags()&^ast.SymbolFlagsTransient == b.Flags()&^ast.SymbolFlagsTransient && a.ValueDeclaration() == b.ValueDeclaration() && len(a.Declarations()) == len(b.Declarations()) -} - -func (c *Checker) verifyMemberLayoutSlot(l *lazyMembers, i int, base *lazyMembers, slot int) { - if slot >= len(base.layout.symbols) || base.layout.key(slot) != l.layout.key(i) { - debug.Fail(fmt.Sprintf("Member layout slot of %q in its base doesn't have the same key", l.layout.key(i))) - } -} From a529cc2fdf4fa442c739364c59b5177b02ee27f4 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:28:42 -0700 Subject: [PATCH 19/28] Remove redundant state from lazy members Every type already has a pointer to its checker, so lazy members don't need their own. Instantiations only have slots for their properties, since unnamed members are never instantiated, and the eager symbol table instantiation is no longer used. --- tsc/internal/checker/checker.go | 21 ++++----------------- tsc/internal/checker/types.go | 8 ++++---- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 5e4a4b75fdc22..4f7b972d0092b 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19609,7 +19609,7 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters return findIndexInfo(indexInfos, info.keyType) == nil })) } - c.setLazyStructuredTypeMembers(t, &lazyMembers{c: c, layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) + c.setLazyStructuredTypeMembers(t, &lazyMembers{layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19696,7 +19696,6 @@ func (l *memberLayout) buildIndex() { // The members of a class or interface type or one of its instantiations. Each member is // resolved on first use and cached in its slot, so full resolution is just resolving every slot. type lazyMembers struct { - c *Checker layout *memberLayout mapper *TypeMapper // Instantiation mapper, nil for the declared type bases []*Type // Base types, instantiated and with this-type arguments @@ -19883,7 +19882,7 @@ func (c *Checker) setLazyStructuredTypeMembers(t *Type, lazy *lazyMembers, callS func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { if l.slots == nil { - l.slots = make([]*ast.Symbol, len(l.layout.symbols)) + l.slots = make([]*ast.Symbol, l.memberCount()) } if symbol := l.slots[i]; symbol != nil { return symbol @@ -19903,7 +19902,8 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { return symbol } -// Returns the number of members, which for instantiations are only the properties (see instantiateSymbolTable). +// Returns the number of members, which for instantiations are only the properties, as members that +// aren't named aren't instantiated. func (l *lazyMembers) memberCount() int { if l.mapper != nil { return l.layout.propertyCount @@ -21537,19 +21537,6 @@ func (c *Checker) createInstantiatedSymbolTable(symbols []*ast.Symbol, m *TypeMa return result } -func (c *Checker) instantiateSymbolTable(symbols ast.SymbolTable, m *TypeMapper) ast.SymbolTable { - if len(symbols) == 0 { - return nil - } - result := make(ast.SymbolTable, len(symbols)) - for id, symbol := range symbols { - if c.isNamedMember(symbol, id) { - result[id] = c.instantiateSymbol(symbol, m) - } - } - return result -} - func (c *Checker) instantiateSymbol(symbol *ast.Symbol, m *TypeMapper) *ast.Symbol { if symbol == nil { return nil diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 6566a9bf524d5..4abb3bf0e9e57 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1167,7 +1167,7 @@ func (t *StructuredType) ensureExtra() *structuredTypeExtra { func (t *StructuredType) Properties() []*ast.Symbol { if t.properties == nil && t.lazy != nil { - t.properties = t.lazy.c.getLazyProperties(t.lazy) + t.properties = t.checker.getLazyProperties(t.lazy) } return t.properties } @@ -1182,21 +1182,21 @@ func (t *StructuredType) propertyShapes() []*ast.Symbol { func (t *StructuredType) property(i int) *ast.Symbol { if t.properties == nil && t.lazy != nil { - return t.lazy.c.getLazyMember(t.lazy, i) + return t.checker.getLazyMember(t.lazy, i) } return t.properties[i] } func (t *StructuredType) getMember(name string) *ast.Symbol { if t.lazy != nil { - return t.lazy.c.getLazyMemberByName(t.lazy, name) + return t.checker.getLazyMemberByName(t.lazy, name) } return t.members[name] } func (t *StructuredType) getMembers() ast.SymbolTable { if t.members == nil && t.lazy != nil { - t.members = t.lazy.c.getLazyMemberTable(t.lazy) + t.members = t.checker.getLazyMemberTable(t.lazy) } return t.members } From 1e71e183fba8e78d568e159818402a338d490774 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:31:57 -0700 Subject: [PATCH 20/28] Find layout members by their symbols' names Class and interface members are always keyed by their symbols' names, so a separate key array only added a case that never happens. An assertion now guards that assumption instead. --- tsc/internal/checker/checker.go | 39 ++++++++++----------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 4f7b972d0092b..7a1605b56f655 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19613,14 +19613,13 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } // A memberLayout describes the members of a class or interface type and of its instantiations: -// their keys, their order, and where each one comes from. Member keys and order don't depend on +// their names, their order, and where each one comes from. Member names and order don't depend on // type arguments when every base type has a shared layout or is any, in which case the layout is // computed once per class or interface and shared by every instantiation. Any other base type, // even one without type parameters, can have different members in each instantiation, for example // an intersection that references the polymorphic this type and reduces to never. type memberLayout struct { symbols []*ast.Symbol // Declared or inherited symbols, with the names and flags of every instantiation's members - keys []string // Member keys, or nil when every key is its symbol's name origins []memberOrigin index []uint32 // Open-addressed hash table of 1-based member indices, or nil for small layouts propertyCount int // The properties are the first members, in property order @@ -19651,17 +19650,10 @@ func (l *memberLayout) properties() []*ast.Symbol { return l.symbols[:l.propertyCount] } -func (l *memberLayout) key(i int) string { - if l.keys != nil { - return l.keys[i] - } - return l.symbols[i].Name() -} - func (l *memberLayout) find(key string) int { if l.index == nil { for i := range l.symbols { - if l.key(i) == key { + if l.symbols[i].Name() == key { return i } } @@ -19669,7 +19661,7 @@ func (l *memberLayout) find(key string) int { } mask := uint64(len(l.index) - 1) for h := xxh3.HashString(key) & mask; l.index[h] != 0; h = (h + 1) & mask { - if i := int(l.index[h] - 1); l.key(i) == key { + if i := int(l.index[h] - 1); l.symbols[i].Name() == key { return i } } @@ -19685,7 +19677,7 @@ func (l *memberLayout) buildIndex() { l.index = make([]uint32, 1< Date: Wed, 14 Oct 2026 21:36:45 -0700 Subject: [PATCH 21/28] Share member layouts between instantiations with the same bases Whether a layout could be shared was decided by a list of special cases: any bases, error bases from the instantiation limit, variadic tuple bases, and this-dependent intersections. Each one was found by a crash or wrong result, and any base type not on the list made the layout unshared. For example, @types/node declares EventTarget with an {} base, so every DOM type above it rebuilt its layout for each instantiation. A layout only depends on the declared members and the members of the bases. Keying it on the bases' layouts, or on the base types themselves when they have no layout, covers all of those cases with one rule and shares layouts whose bases are the same non-generic type. --- tsc/internal/checker/checker.go | 106 ++++++++++++-------------------- 1 file changed, 41 insertions(+), 65 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 7a1605b56f655..3918d33727f06 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19614,22 +19614,21 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters // A memberLayout describes the members of a class or interface type and of its instantiations: // their names, their order, and where each one comes from. Member names and order don't depend on -// type arguments when every base type has a shared layout or is any, in which case the layout is -// computed once per class or interface and shared by every instantiation. Any other base type, -// even one without type parameters, can have different members in each instantiation, for example -// an intersection that references the polymorphic this type and reduces to never. +// type arguments, only on the declared members and the members of the base types. So instantiations +// whose base types have the same members, because they have the same layout or are the same type, +// share the layout, which is computed once in the common case where every instantiation's bases do. type memberLayout struct { symbols []*ast.Symbol // Declared or inherited symbols, with the names and flags of every instantiation's members origins []memberOrigin index []uint32 // Open-addressed hash table of 1-based member indices, or nil for small layouts propertyCount int // The properties are the first members, in property order - shared bool - bases []memberLayoutBase // The bases the layout was built from, which a shared layout's instantiations must match + bases []memberLayoutBase } +// The base type members a layout was built from, which determine the inherited members. type memberLayoutBase struct { layout *memberLayout // Layout of a base type with one, through which inherited members are found - isAny bool + t *Type // Any other base type, whose properties are the inherited members } type memberOrigin struct { @@ -19643,7 +19642,7 @@ type memberOriginKind uint8 const ( memberOriginDeclared memberOriginKind = iota // Declared member, instantiated with the type's mapper memberOriginBaseLayout // Member of the base type, through the base type's layout - memberOriginFixed // The symbol itself, in a layout that isn't shared + memberOriginFixed // The symbol itself, a property of a base type without a layout ) func (l *memberLayout) properties() []*ast.Symbol { @@ -19696,36 +19695,32 @@ type lazyMembers struct { func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { d := source.AsInterfaceType() - if d.memberLayout == nil { - layout := c.buildMemberLayout(source, bases) - if layout.shared { - d.memberLayout = layout - } + if layout := d.memberLayout; layout != nil && c.hasMemberLayoutBases(layout, bases) { return layout } - if !c.canShareMemberLayout(d.memberLayout, bases) { - return c.buildMemberLayout(source, bases) + layout := c.buildMemberLayout(source, bases) + // A layout computed while base types are still being resolved may be missing inherited members. + if d.baseTypesResolved { + d.memberLayout = layout } - return d.memberLayout + return layout } -// Reports whether the bases of an instantiation have the layouts the shared layout was built from, since -// inherited members are found through their slots in those layouts. Instantiations of a variadic tuple -// base can have different tuple targets, a base type instantiation that hit an instantiation limit is -// the error type, and a base whose own base hit the limit has a smaller layout that isn't shared. -func (c *Checker) canShareMemberLayout(layout *memberLayout, bases []*Type) bool { - for k, base := range bases { - expected := layout.bases[k] - if base == c.anyType || expected.isAny { - if base != c.anyType || !expected.isAny { - return false - } - continue +func (c *Checker) getMemberLayoutBase(base *Type) memberLayoutBase { + if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { + if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { + return memberLayoutBase{layout: lazy.layout} } - if base.flags&TypeFlagsObject == 0 || base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) == 0 { - return false - } - if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy == nil || lazy.layout != expected.layout { + } + return memberLayoutBase{t: base} +} + +func (c *Checker) hasMemberLayoutBases(layout *memberLayout, bases []*Type) bool { + if len(layout.bases) != len(bases) { + return false + } + for k, base := range bases { + if c.getMemberLayoutBase(base) != layout.bases[k] { return false } } @@ -19748,35 +19743,19 @@ const ( ) func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { - type baseMembers struct { - layout *memberLayout - properties []*ast.Symbol - kind memberOriginKind - } d := source.AsInterfaceType() - // A layout computed while base types are still being resolved may be missing inherited members. - shared := d.baseTypesResolved // Resolve the bases first, since this may build other layouts with the scratch storage below. - baseMembersList := make([]baseMembers, len(bases)) - capacity := len(d.declaredMembers) layoutBases := make([]memberLayoutBase, len(bases)) + baseProperties := make([][]*ast.Symbol, len(bases)) + capacity := len(d.declaredMembers) for k, base := range bases { - if base == c.anyType { - layoutBases[k].isAny = true - continue - } - if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { - if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { - shared = shared && lazy.layout.shared - baseMembersList[k] = baseMembers{layout: lazy.layout, kind: memberOriginBaseLayout} - layoutBases[k].layout = lazy.layout - capacity += lazy.layout.propertyCount - continue - } + layoutBases[k] = c.getMemberLayoutBase(base) + if baseLayout := layoutBases[k].layout; baseLayout != nil { + baseProperties[k] = baseLayout.properties() + } else { + baseProperties[k] = c.getPropertiesOfType(base) } - shared = false - baseMembersList[k] = baseMembers{properties: c.getPropertiesOfType(base), kind: memberOriginFixed} - capacity += len(baseMembersList[k].properties) + capacity += len(baseProperties[k]) } // Reuse scratch storage, since this runs for every class and interface. entries, index := c.layoutEntries[:0], c.layoutIndex @@ -19793,16 +19772,16 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { entries = append(entries, layoutEntry{symbol: symbol, origin: memberOrigin{kind: memberOriginDeclared}}) } // Same as addInheritedMembers. - for k, base := range baseMembersList { - properties := base.properties - if base.layout != nil { - properties = base.layout.properties() + for k, properties := range baseProperties { + kind := memberOriginFixed + if layoutBases[k].layout != nil { + kind = memberOriginBaseLayout } for i, symbol := range properties { if isStaticPrivateIdentifierProperty(symbol) { continue } - entry := layoutEntry{symbol: symbol, origin: memberOrigin{kind: base.kind, base: int32(k), slot: int32(i)}} + entry := layoutEntry{symbol: symbol, origin: memberOrigin{kind: kind, base: int32(k), slot: int32(i)}} if j, ok := index[symbol.Name()]; !ok { index[symbol.Name()] = len(entries) entries = append(entries, entry) @@ -19840,10 +19819,7 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { symbols: make([]*ast.Symbol, len(entries)), origins: make([]memberOrigin, len(entries)), propertyCount: propertyCount, - shared: shared, - } - if shared { - layout.bases = layoutBases + bases: layoutBases, } for i, e := range entries { layout.symbols[i] = e.symbol From b8f7c08d8a677c2a8c5ee0b9beff8bdcf2f3b72b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:40:00 -0700 Subject: [PATCH 22/28] Find inherited members through the bases a layout was chosen for Inherited members were found by resolving the base type again and indexing its current members with the slot from the layout. That's only correct if the base still has the layout this layout was built from, which held but wasn't guaranteed by anything local. Keeping the base members that were used to choose the layout makes the slots always refer to the right layout, and matches addInheritedMembers, which copies the base members as they are when the type is resolved. --- tsc/internal/checker/checker.go | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 3918d33727f06..5637f9047769e 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19595,7 +19595,7 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } // Members are resolved on demand through a layout that is computed once and shared by all instantiations // when possible. The layout must be computed before inherited signatures, which may circularly depend on it. - layout := c.getMemberLayout(source, instantiatedBaseTypes) + layout, baseMembers := c.getMemberLayout(source, instantiatedBaseTypes) for _, instantiatedBaseType := range instantiatedBaseTypes { callSignatures = core.Concatenate(callSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindCall)) constructSignatures = core.Concatenate(constructSignatures, c.getSignaturesOfType(instantiatedBaseType, SignatureKindConstruct)) @@ -19609,7 +19609,7 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters return findIndexInfo(indexInfos, info.keyType) == nil })) } - c.setLazyStructuredTypeMembers(t, &lazyMembers{layout: layout, mapper: mapper, bases: instantiatedBaseTypes}, callSignatures, constructSignatures, indexInfos) + c.setLazyStructuredTypeMembers(t, &lazyMembers{layout: layout, mapper: mapper, bases: baseMembers}, callSignatures, constructSignatures, indexInfos) } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19689,38 +19689,49 @@ func (l *memberLayout) buildIndex() { type lazyMembers struct { layout *memberLayout mapper *TypeMapper // Instantiation mapper, nil for the declared type - bases []*Type // Base types, instantiated and with this-type arguments + bases []*lazyMembers // Members of the base types with the layout's base layouts, or nil when there are none slots []*ast.Symbol } -func (c *Checker) getMemberLayout(source *Type, bases []*Type) *memberLayout { +// Returns the layout for the given base types, along with the lazy members of the bases that have +// layouts. Inherited members are found through those, so that they always match the layout's slots. +func (c *Checker) getMemberLayout(source *Type, bases []*Type) (*memberLayout, []*lazyMembers) { + var baseMembers []*lazyMembers + for k, base := range bases { + if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { + if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { + if baseMembers == nil { + baseMembers = make([]*lazyMembers, len(bases)) + } + baseMembers[k] = lazy + } + } + } d := source.AsInterfaceType() - if layout := d.memberLayout; layout != nil && c.hasMemberLayoutBases(layout, bases) { - return layout + if layout := d.memberLayout; layout != nil && hasMemberLayoutBases(layout, bases, baseMembers) { + return layout, baseMembers } - layout := c.buildMemberLayout(source, bases) + layout := c.buildMemberLayout(source, bases, baseMembers) // A layout computed while base types are still being resolved may be missing inherited members. if d.baseTypesResolved { d.memberLayout = layout } - return layout + return layout, baseMembers } -func (c *Checker) getMemberLayoutBase(base *Type) memberLayoutBase { - if base.flags&TypeFlagsObject != 0 && base.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { - if lazy := c.resolveStructuredTypeMembers(base).lazy; lazy != nil { - return memberLayoutBase{layout: lazy.layout} - } +func getMemberLayoutBase(bases []*Type, baseMembers []*lazyMembers, k int) memberLayoutBase { + if baseMembers != nil && baseMembers[k] != nil { + return memberLayoutBase{layout: baseMembers[k].layout} } - return memberLayoutBase{t: base} + return memberLayoutBase{t: bases[k]} } -func (c *Checker) hasMemberLayoutBases(layout *memberLayout, bases []*Type) bool { +func hasMemberLayoutBases(layout *memberLayout, bases []*Type, baseMembers []*lazyMembers) bool { if len(layout.bases) != len(bases) { return false } - for k, base := range bases { - if c.getMemberLayoutBase(base) != layout.bases[k] { + for k := range bases { + if getMemberLayoutBase(bases, baseMembers, k) != layout.bases[k] { return false } } @@ -19742,14 +19753,14 @@ const ( memberRankUnnamed // Then members that aren't properties ) -func (c *Checker) buildMemberLayout(source *Type, bases []*Type) *memberLayout { +func (c *Checker) buildMemberLayout(source *Type, bases []*Type, baseMembers []*lazyMembers) *memberLayout { d := source.AsInterfaceType() // Resolve the bases first, since this may build other layouts with the scratch storage below. layoutBases := make([]memberLayoutBase, len(bases)) baseProperties := make([][]*ast.Symbol, len(bases)) capacity := len(d.declaredMembers) for k, base := range bases { - layoutBases[k] = c.getMemberLayoutBase(base) + layoutBases[k] = getMemberLayoutBase(bases, baseMembers, k) if baseLayout := layoutBases[k].layout; baseLayout != nil { baseProperties[k] = baseLayout.properties() } else { @@ -19853,9 +19864,7 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { symbol = c.instantiateSymbol(symbol, l.mapper) } case memberOriginBaseLayout: - base := c.resolveStructuredTypeMembers(l.bases[origin.base]).lazy - debug.Assert(base != nil, "Base type must have lazy members") - symbol = c.getLazyMember(base, int(origin.slot)) + symbol = c.getLazyMember(l.bases[origin.base], int(origin.slot)) } l.slots[i] = symbol return symbol From a0d94c2696bbf9d363ac9f3e4d8f4319b1b67af3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:47:47 -0700 Subject: [PATCH 23/28] Order layout members with getNamedMembers The layout order must match the order getNamedMembers gives resolved members, and it was produced by a separate ranking and sort that had to be kept in sync with getNamedMembers by hand. Building the same symbol table addInheritedMembers would and passing it to getNamedMembers makes the order the same by construction. --- tsc/internal/checker/checker.go | 92 ++++++++++----------------------- 1 file changed, 26 insertions(+), 66 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 5637f9047769e..f7003ca876f95 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -594,14 +594,14 @@ type Checker struct { fileIndexMap map[*ast.SourceFile]int compareSymbols func(*ast.Symbol, *ast.Symbol) int compareSymbolChains func([]*ast.Symbol, []*ast.Symbol) int + layoutMembers ast.SymbolTable + layoutOrigins map[string]memberOrigin TypeCount uint32 SymbolCount uint32 SignatureCount uint32 TotalInstantiationCount uint32 instantiationCount uint32 instantiationStack []*Type - layoutEntries []layoutEntry - layoutIndex map[string]int conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -19688,7 +19688,7 @@ func (l *memberLayout) buildIndex() { // resolved on first use and cached in its slot, so full resolution is just resolving every slot. type lazyMembers struct { layout *memberLayout - mapper *TypeMapper // Instantiation mapper, nil for the declared type + mapper *TypeMapper // Instantiation mapper, nil for the declared type bases []*lazyMembers // Members of the base types with the layout's base layouts, or nil when there are none slots []*ast.Symbol } @@ -19738,24 +19738,8 @@ func hasMemberLayoutBases(layout *memberLayout, bases []*Type, baseMembers []*la return true } -type layoutEntry struct { - symbol *ast.Symbol - origin memberOrigin - rank memberRank -} - -// Members are ordered as in getNamedMembers. -type memberRank uint8 - -const ( - memberRankDeclared memberRank = iota // Properties declared in a class or interface come first - memberRankInherited // Then other properties - memberRankUnnamed // Then members that aren't properties -) - func (c *Checker) buildMemberLayout(source *Type, bases []*Type, baseMembers []*lazyMembers) *memberLayout { d := source.AsInterfaceType() - // Resolve the bases first, since this may build other layouts with the scratch storage below. layoutBases := make([]memberLayoutBase, len(bases)) baseProperties := make([][]*ast.Symbol, len(bases)) capacity := len(d.declaredMembers) @@ -19768,19 +19752,17 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type, baseMembers []* } capacity += len(baseProperties[k]) } - // Reuse scratch storage, since this runs for every class and interface. - entries, index := c.layoutEntries[:0], c.layoutIndex - c.layoutEntries, c.layoutIndex = nil, nil - if cap(entries) < capacity { - entries = make([]layoutEntry, 0, capacity) - } - if index == nil { - index = make(map[string]int, capacity) + // Reuse scratch maps, since this runs for every class and interface. They're taken rather than shared, + // since building the layout can build other layouts. + members, origins := c.layoutMembers, c.layoutOrigins + c.layoutMembers, c.layoutOrigins = nil, nil + if members == nil { + members, origins = make(ast.SymbolTable, capacity), make(map[string]memberOrigin, capacity) } for key, symbol := range d.declaredMembers { - index[key] = len(entries) debug.Assert(key == symbol.Name(), "Member key must be its symbol's name") - entries = append(entries, layoutEntry{symbol: symbol, origin: memberOrigin{kind: memberOriginDeclared}}) + members[key] = symbol + origins[key] = memberOrigin{kind: memberOriginDeclared} } // Same as addInheritedMembers. for k, properties := range baseProperties { @@ -19792,53 +19774,31 @@ func (c *Checker) buildMemberLayout(source *Type, bases []*Type, baseMembers []* if isStaticPrivateIdentifierProperty(symbol) { continue } - entry := layoutEntry{symbol: symbol, origin: memberOrigin{kind: kind, base: int32(k), slot: int32(i)}} - if j, ok := index[symbol.Name()]; !ok { - index[symbol.Name()] = len(entries) - entries = append(entries, entry) - } else if entries[j].symbol.Flags()&ast.SymbolFlagsValue == 0 { - entries[j] = entry + if existing, ok := members[symbol.Name()]; !ok || existing.Flags()&ast.SymbolFlagsValue == 0 { + members[symbol.Name()] = symbol + origins[symbol.Name()] = memberOrigin{kind: kind, base: int32(k), slot: int32(i)} } } } - container := source.symbol - isClassOrInterface := container != nil && container.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 - propertyCount := 0 - for i := range entries { - e := &entries[i] - switch { - case !c.isNamedMember(e.symbol, e.symbol.Name()): - e.rank = memberRankUnnamed - continue - case isClassOrInterface && c.isDeclarationContainedBy(e.symbol, container): - e.rank = memberRankDeclared - default: - e.rank = memberRankInherited + symbols := c.getNamedMembers(members, source.symbol) + propertyCount := len(symbols) + for name, symbol := range members { + if !c.isNamedMember(symbol, name) { + symbols = append(symbols, symbol) } - propertyCount++ } - slices.SortFunc(entries, func(a, b layoutEntry) int { - if a.rank != b.rank { - return int(a.rank) - int(b.rank) - } - if a.rank == memberRankUnnamed { - return 0 - } - return c.compareSymbols(a.symbol, b.symbol) - }) layout := &memberLayout{ - symbols: make([]*ast.Symbol, len(entries)), - origins: make([]memberOrigin, len(entries)), + symbols: symbols, + origins: make([]memberOrigin, len(symbols)), propertyCount: propertyCount, bases: layoutBases, } - for i, e := range entries { - layout.symbols[i] = e.symbol - layout.origins[i] = e.origin + for i, symbol := range symbols { + layout.origins[i] = origins[symbol.Name()] } - clear(entries) - clear(index) - c.layoutEntries, c.layoutIndex = entries, index + clear(members) + clear(origins) + c.layoutMembers, c.layoutOrigins = members, origins layout.buildIndex() return layout } From 13405d511d1cf402455a4852e21d91d126fa8240 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:54:41 -0700 Subject: [PATCH 24/28] Reapply "Verify member layouts against eager member resolution" Turn the check back on for a run over the top 1000 repos, to confirm the simplified layout sharing and ordering still match eager member resolution. It will be reverted again afterwards. --- tsc/internal/checker/checker.go | 10 ++ tsc/internal/checker/memberlayoutverify.go | 109 +++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tsc/internal/checker/memberlayoutverify.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index f7003ca876f95..da3c287493283 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -602,6 +602,7 @@ type Checker struct { TotalInstantiationCount uint32 instantiationCount uint32 instantiationStack []*Type + memberLayoutVerifications []memberLayoutVerification conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -14232,6 +14233,9 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile c.checkNotCanceled() checkUnused := c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue() || collection == &c.suggestionDiagnostics c.checkSourceFile(ctx, sourceFile, checkUnused) + if verifyMemberLayouts { + c.verifyMemberLayouts() + } if c.wasCanceled { return nil } @@ -19610,6 +19614,9 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters })) } c.setLazyStructuredTypeMembers(t, &lazyMembers{layout: layout, mapper: mapper, bases: baseMembers}, callSignatures, constructSignatures, indexInfos) + if verifyMemberLayouts { + c.recordMemberLayoutVerification(t, source, instantiatedBaseTypes) + } } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19824,6 +19831,9 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { symbol = c.instantiateSymbol(symbol, l.mapper) } case memberOriginBaseLayout: + if verifyMemberLayouts { + c.verifyMemberLayoutSlot(l, i, int(origin.base), int(origin.slot)) + } symbol = c.getLazyMember(l.bases[origin.base], int(origin.slot)) } l.slots[i] = symbol diff --git a/tsc/internal/checker/memberlayoutverify.go b/tsc/internal/checker/memberlayoutverify.go new file mode 100644 index 0000000000000..eff9214e59b8b --- /dev/null +++ b/tsc/internal/checker/memberlayoutverify.go @@ -0,0 +1,109 @@ +package checker + +import ( + "fmt" + "maps" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/debug" +) + +// When set, every class or interface type whose members are resolved through a member layout is +// checked against members resolved eagerly from the same declared members, mapper and base types, +// as they were before member layouts. The check runs after each source file is checked, so that +// it doesn't change the order in which types are resolved during checking. +const verifyMemberLayouts = true + +type memberLayoutVerification struct { + t *Type + declaredMembers ast.SymbolTable // The declared members when resolved, which can be partial during late binding + bases []*Type + lazy *lazyMembers +} + +func (c *Checker) recordMemberLayoutVerification(t *Type, source *Type, bases []*Type) { + c.memberLayoutVerifications = append(c.memberLayoutVerifications, memberLayoutVerification{t: t, declaredMembers: source.AsInterfaceType().declaredMembers, bases: bases, lazy: t.AsStructuredType().lazy}) +} + +func (c *Checker) verifyMemberLayouts() { + // Verification can resolve more types, which records them to be verified too. + for len(c.memberLayoutVerifications) != 0 { + pending := c.memberLayoutVerifications + c.memberLayoutVerifications = nil + for _, v := range pending { + c.verifyMemberLayout(v) + } + } +} + +func (c *Checker) verifyMemberLayout(v memberLayoutVerification) { + fail := func(format string, args ...any) { + debug.Fail(fmt.Sprintf("Member layout of %s doesn't match its eager members: %s", c.TypeToString(v.t), fmt.Sprintf(format, args...))) + } + l := v.lazy + if v.t.AsStructuredType().lazy != l { + // Members resolved again during a circular resolution replace these, and are verified separately. + return + } + members := maps.Clone(v.declaredMembers) + if l.mapper != nil { + // Members that aren't named, like signatures, are resolved separately for instantiations. + for key, symbol := range members { + if c.isNamedMember(symbol, key) { + members[key] = c.instantiateSymbol(symbol, l.mapper) + } else { + delete(members, key) + } + } + } + for _, base := range v.bases { + members = c.addInheritedMembers(members, c.getPropertiesOfType(base)) + } + eager := c.getNamedMembers(members, v.t.symbol) + properties := c.getLazyProperties(l) + if len(eager) != len(properties) { + fail("%d properties, expected %d", len(properties), len(eager)) + } + for i, e := range eager { + p := properties[i] + if p.Name() != e.Name() { + fail("property %d is %q, expected %q", i, p.Name(), e.Name()) + } + // Instantiation can return either the original or a new symbol depending on what was resolved + // first, so instantiated symbols are compared by their flags and declarations. + if p != e && !sameMemberSymbol(p, e) { + fail("property %q has a different symbol", e.Name()) + } + } + table := c.getLazyMemberTable(l) + if len(table) != len(members) { + fail("%d members, expected %d", len(table), len(members)) + } + for key, e := range members { + p, ok := table[key] + if !ok { + fail("member %q is missing", key) + } + if p.Name() != e.Name() || !sameMemberSymbol(p, e) { + fail("member %q has a different symbol", key) + } + if c.getLazyMemberByName(l, key) != p { + fail("member %q is found by name as a different symbol", key) + } + } +} + +func sameMemberSymbol(a *ast.Symbol, b *ast.Symbol) bool { + return a.Flags()&^ast.SymbolFlagsTransient == b.Flags()&^ast.SymbolFlagsTransient && a.ValueDeclaration() == b.ValueDeclaration() && len(a.Declarations()) == len(b.Declarations()) +} + +func (c *Checker) verifyMemberLayoutSlot(l *lazyMembers, i int, k int, slot int) { + name := l.layout.symbols[i].Name() + base := l.bases[k] + if base == nil || base.layout != l.layout.bases[k].layout { + debug.Fail(fmt.Sprintf("Member layout base of %q isn't the layout it was built from", name)) + } + if slot >= len(base.layout.symbols) || base.layout.symbols[slot].Name() != name { + debug.Fail(fmt.Sprintf("Member layout slot of %q in its base doesn't have the same name", name)) + } +} From 14a87898ca1d9e4f3ff3d98dd37303634c3134ec Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:53:31 -0700 Subject: [PATCH 25/28] Revert "Reapply "Verify member layouts against eager member resolution"" The top 1000 and user test runs found no mismatches, so the check is no longer needed. --- tsc/internal/checker/checker.go | 10 -- tsc/internal/checker/memberlayoutverify.go | 109 --------------------- 2 files changed, 119 deletions(-) delete mode 100644 tsc/internal/checker/memberlayoutverify.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index da3c287493283..f7003ca876f95 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -602,7 +602,6 @@ type Checker struct { TotalInstantiationCount uint32 instantiationCount uint32 instantiationStack []*Type - memberLayoutVerifications []memberLayoutVerification conditionalConstraintDepth uint32 inlineLevel int serializationLevel int @@ -14233,9 +14232,6 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile c.checkNotCanceled() checkUnused := c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue() || collection == &c.suggestionDiagnostics c.checkSourceFile(ctx, sourceFile, checkUnused) - if verifyMemberLayouts { - c.verifyMemberLayouts() - } if c.wasCanceled { return nil } @@ -19614,9 +19610,6 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters })) } c.setLazyStructuredTypeMembers(t, &lazyMembers{layout: layout, mapper: mapper, bases: baseMembers}, callSignatures, constructSignatures, indexInfos) - if verifyMemberLayouts { - c.recordMemberLayoutVerification(t, source, instantiatedBaseTypes) - } } // A memberLayout describes the members of a class or interface type and of its instantiations: @@ -19831,9 +19824,6 @@ func (c *Checker) getLazyMember(l *lazyMembers, i int) *ast.Symbol { symbol = c.instantiateSymbol(symbol, l.mapper) } case memberOriginBaseLayout: - if verifyMemberLayouts { - c.verifyMemberLayoutSlot(l, i, int(origin.base), int(origin.slot)) - } symbol = c.getLazyMember(l.bases[origin.base], int(origin.slot)) } l.slots[i] = symbol diff --git a/tsc/internal/checker/memberlayoutverify.go b/tsc/internal/checker/memberlayoutverify.go deleted file mode 100644 index eff9214e59b8b..0000000000000 --- a/tsc/internal/checker/memberlayoutverify.go +++ /dev/null @@ -1,109 +0,0 @@ -package checker - -import ( - "fmt" - "maps" - - "github.com/microsoft/TypeScript/tsc/internal/ast" - "github.com/microsoft/TypeScript/tsc/internal/debug" -) - -// When set, every class or interface type whose members are resolved through a member layout is -// checked against members resolved eagerly from the same declared members, mapper and base types, -// as they were before member layouts. The check runs after each source file is checked, so that -// it doesn't change the order in which types are resolved during checking. -const verifyMemberLayouts = true - -type memberLayoutVerification struct { - t *Type - declaredMembers ast.SymbolTable // The declared members when resolved, which can be partial during late binding - bases []*Type - lazy *lazyMembers -} - -func (c *Checker) recordMemberLayoutVerification(t *Type, source *Type, bases []*Type) { - c.memberLayoutVerifications = append(c.memberLayoutVerifications, memberLayoutVerification{t: t, declaredMembers: source.AsInterfaceType().declaredMembers, bases: bases, lazy: t.AsStructuredType().lazy}) -} - -func (c *Checker) verifyMemberLayouts() { - // Verification can resolve more types, which records them to be verified too. - for len(c.memberLayoutVerifications) != 0 { - pending := c.memberLayoutVerifications - c.memberLayoutVerifications = nil - for _, v := range pending { - c.verifyMemberLayout(v) - } - } -} - -func (c *Checker) verifyMemberLayout(v memberLayoutVerification) { - fail := func(format string, args ...any) { - debug.Fail(fmt.Sprintf("Member layout of %s doesn't match its eager members: %s", c.TypeToString(v.t), fmt.Sprintf(format, args...))) - } - l := v.lazy - if v.t.AsStructuredType().lazy != l { - // Members resolved again during a circular resolution replace these, and are verified separately. - return - } - members := maps.Clone(v.declaredMembers) - if l.mapper != nil { - // Members that aren't named, like signatures, are resolved separately for instantiations. - for key, symbol := range members { - if c.isNamedMember(symbol, key) { - members[key] = c.instantiateSymbol(symbol, l.mapper) - } else { - delete(members, key) - } - } - } - for _, base := range v.bases { - members = c.addInheritedMembers(members, c.getPropertiesOfType(base)) - } - eager := c.getNamedMembers(members, v.t.symbol) - properties := c.getLazyProperties(l) - if len(eager) != len(properties) { - fail("%d properties, expected %d", len(properties), len(eager)) - } - for i, e := range eager { - p := properties[i] - if p.Name() != e.Name() { - fail("property %d is %q, expected %q", i, p.Name(), e.Name()) - } - // Instantiation can return either the original or a new symbol depending on what was resolved - // first, so instantiated symbols are compared by their flags and declarations. - if p != e && !sameMemberSymbol(p, e) { - fail("property %q has a different symbol", e.Name()) - } - } - table := c.getLazyMemberTable(l) - if len(table) != len(members) { - fail("%d members, expected %d", len(table), len(members)) - } - for key, e := range members { - p, ok := table[key] - if !ok { - fail("member %q is missing", key) - } - if p.Name() != e.Name() || !sameMemberSymbol(p, e) { - fail("member %q has a different symbol", key) - } - if c.getLazyMemberByName(l, key) != p { - fail("member %q is found by name as a different symbol", key) - } - } -} - -func sameMemberSymbol(a *ast.Symbol, b *ast.Symbol) bool { - return a.Flags()&^ast.SymbolFlagsTransient == b.Flags()&^ast.SymbolFlagsTransient && a.ValueDeclaration() == b.ValueDeclaration() && len(a.Declarations()) == len(b.Declarations()) -} - -func (c *Checker) verifyMemberLayoutSlot(l *lazyMembers, i int, k int, slot int) { - name := l.layout.symbols[i].Name() - base := l.bases[k] - if base == nil || base.layout != l.layout.bases[k].layout { - debug.Fail(fmt.Sprintf("Member layout base of %q isn't the layout it was built from", name)) - } - if slot >= len(base.layout.symbols) || base.layout.symbols[slot].Name() != name { - debug.Fail(fmt.Sprintf("Member layout slot of %q in its base doesn't have the same name", name)) - } -} From 46b83d76c156b36198b3f5ea24d45444b2eb91f1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:01:04 -0700 Subject: [PATCH 26/28] Remove the instantiation limit member layout test The test needed test-only hooks to fake reaching the instantiation limit and to build one specific base type instantiation. It guarded a special case in the old layout sharing rules; layouts are now keyed by their bases, so a base that hits the limit is just a different key and there is no special case left to regress. --- tsc/internal/checker/export_test.go | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 tsc/internal/checker/export_test.go diff --git a/tsc/internal/checker/export_test.go b/tsc/internal/checker/export_test.go deleted file mode 100644 index 22b9b44ad5950..0000000000000 --- a/tsc/internal/checker/export_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package checker - -// RunAtInstantiationLimit runs f as if the current statement had reached the instantiation count limit. -func (c *Checker) RunAtInstantiationLimit(f func()) { - saved := c.instantiationCount - c.instantiationCount = 5_000_000 - defer func() { c.instantiationCount = saved }() - f() -} - -func (c *Checker) GetTypeWithThisArgument(t *Type, thisArgument *Type) *Type { - return c.getTypeWithThisArgument(t, thisArgument, false /*needsApparentType*/) -} From ec3709824ec415f25a42129282a7100da0ec90fa Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:01:59 -0700 Subject: [PATCH 27/28] Remove the test that used the instantiation limit hooks The previous commit removed the hooks but not the test that used them. --- tsc/internal/checker/checker_test.go | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 542db57875e71..31cea19751525 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -208,38 +208,3 @@ export type Initial = Box<"first">; assert.Equal(t, c.TypeToString(initial), "Initial") assert.Equal(t, len(c.GetDiagnostics(t.Context(), file)), 0) } - -func TestMemberLayoutAfterBaseInstantiationLimit(t *testing.T) { - t.Parallel() - fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ - "/foo.ts": `interface Grand { grand: T } -interface Base extends Grand { base: T } -interface Derived extends Base { derived: T } -export type S = Derived; -export type N = Derived; -export type B = Base; -`, - }, tspath.CaseSensitive)) - opts := &core.CompilerOptions{NoLib: core.TSTrue} - program := compiler.NewProgram(compiler.ProgramOptions{ - Config: tsoptions.NewParsedCommandLine(opts, []tspath.RootedFilePath{"/foo.ts"}, nil, "/", fs.CaseSensitivity()), - Host: compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil), - }) - program.BindSourceFiles() - c, done := program.GetTypeChecker(t.Context()) - defer done() - file := program.GetSourceFile("/foo.ts") - typeOfAlias := func(i int) *checker.Type { - return c.GetTypeAtLocation(file.Statements.Nodes[i].AsTypeAliasDeclaration().Type) - } - // Share the member layouts of Derived, Base, and Grand. - assert.Equal(t, len(c.GetPropertiesOfType(typeOfAlias(3))), 3) - // Resolve the base of Derived after instantiating Grand has hit the limit. - derived := typeOfAlias(4) - base := c.GetTypeWithThisArgument(typeOfAlias(5), derived) - c.RunAtInstantiationLimit(func() { - assert.Equal(t, len(c.GetPropertiesOfType(base)), 1) - }) - names := core.Map(c.GetPropertiesOfType(derived), (*ast.Symbol).Name) - assert.DeepEqual(t, names, []string{"derived", "base"}) -} From 2c9f837f5085abd8a60bfce5f74d73dc08f97158 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:08:15 -0700 Subject: [PATCH 28/28] Rename property shapes to uninstantiated properties "Shapes" suggested special symbols that only have names and flags. They're the real symbols that an instantiation's properties would be instantiated from, so their types are wrong for the instantiation. The name should say that, so it isn't used for anything but names and flags. --- tsc/internal/checker/checker.go | 16 +++++++++------- tsc/internal/checker/relater.go | 4 ++-- tsc/internal/checker/types.go | 5 +++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index f7003ca876f95..0fbafb0588502 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19291,11 +19291,13 @@ func (c *Checker) getPropertiesOfType(t *Type) []*ast.Symbol { return c.getPropertiesOfObjectType(t) } -// Like getPropertiesOfType, but returns symbols with only the right names and flags, without resolving lazy members. -func (c *Checker) getPropertyShapesOfType(t *Type) []*ast.Symbol { +// Like getPropertiesOfType, but without resolving lazy members, so the properties of an instantiated class or +// interface may be the symbols they would be instantiated from. These have the same names and flags as the +// actual properties, but not the same types, so only use this for names and flags. +func (c *Checker) getUninstantiatedPropertiesOfType(t *Type) []*ast.Symbol { t = c.getReducedApparentType(t) if t.flags&TypeFlagsObject != 0 { - return c.resolveStructuredTypeMembers(t).propertyShapes() + return c.resolveStructuredTypeMembers(t).uninstantiatedProperties() } return c.getPropertiesOfType(t) } @@ -20067,7 +20069,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.propertyShapes()) == 0 && len(resolved.getIndexInfos()) == 0 { + if allowMembers || len(resolved.uninstantiatedProperties()) == 0 && len(resolved.getIndexInfos()) == 0 { if kind == SignatureKindCall && len(resolved.CallSignatures()) == 1 && len(resolved.ConstructSignatures()) == 0 { return resolved.CallSignatures()[0] } @@ -22592,7 +22594,7 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { counts := c.getTemporaryPropertyCounts() defer c.releaseTemporaryPropertyCounts(counts) for _, t := range t.Types() { - for _, prop := range c.getPropertyShapesOfType(t) { + for _, prop := range c.getUninstantiatedPropertiesOfType(t) { counts.Set(prop.Name(), counts.GetOrZero(prop.Name())+1) } } @@ -27350,7 +27352,7 @@ func (c *Checker) IsEmptyAnonymousObjectType(t *Type) bool { } func (c *Checker) isEmptyResolvedType(t *StructuredType) bool { - return t.AsType() != c.anyFunctionType && len(t.propertyShapes()) == 0 && len(t.getSignatures()) == 0 && len(t.getIndexInfos()) == 0 + return t.AsType() != c.anyFunctionType && len(t.uninstantiatedProperties()) == 0 && len(t.getSignatures()) == 0 && len(t.getIndexInfos()) == 0 } func (c *Checker) isEmptyObjectType(t *Type) bool { @@ -28238,7 +28240,7 @@ func (c *Checker) getPropertyNameFromIndex(indexType *Type, accessNode *ast.Node } func (c *Checker) isStringIndexSignatureOnlyTypeWorker(t *Type) bool { - return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && len(c.getPropertyShapesOfType(t)) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || + return t.flags&TypeFlagsObject != 0 && !c.isGenericMappedType(t) && len(c.getUninstantiatedPropertiesOfType(t)) == 0 && len(c.getIndexInfosOfType(t)) == 1 && c.getIndexInfoOfType(t, c.stringType) != nil || t.flags&TypeFlagsUnionOrIntersection != 0 && core.Every(t.Types(), c.isStringIndexSignatureOnlyType) } diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 1a9d80f0c624f..35f1cd09f234d 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) - properties := resolved.propertyShapes() + properties := resolved.uninstantiatedProperties() return len(resolved.getSignatures()) == 0 && len(resolved.getIndexInfos()) == 0 && len(properties) > 0 && core.Every(properties, func(p *ast.Symbol) bool { return p.Flags()&ast.SymbolFlagsOptional != 0 }) @@ -979,7 +979,7 @@ func (c *Checker) getUnmatchedPropertiesWorker(source *Type, target *Type, requi // Names and flags of properties are known without resolving them, so only resolve the ones we report or compare. // Reduce the target once, since getting its apparent type can allocate. target = c.getReducedApparentType(target) - for i, targetProp := range c.getPropertyShapesOfType(target) { + for i, targetProp := range c.getUninstantiatedPropertiesOfType(target) { // TODO: remove this when we support static private identifier fields and find other solutions to get privateNamesAndStaticFields test to pass if isStaticPrivateIdentifierProperty(targetProp) { continue diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 4abb3bf0e9e57..e6ae20686b15f 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1172,8 +1172,9 @@ func (t *StructuredType) Properties() []*ast.Symbol { return t.properties } -// Like Properties, but returns symbols with only the right names and flags, without resolving lazy members. -func (t *StructuredType) propertyShapes() []*ast.Symbol { +// Like Properties, but without resolving lazy members, so the properties may be the symbols they would be +// instantiated from. These have the same names and flags as the actual properties, but not the same types. +func (t *StructuredType) uninstantiatedProperties() []*ast.Symbol { if t.properties == nil && t.lazy != nil { return t.lazy.layout.properties() }