diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 10ddb3c0c88b4..22c9597ee5198 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -692,6 +692,7 @@ type Checker struct { structuredTypeExtraArena core.Arena[structuredTypeExtra] freeTypeSets *temporaryTypeSet freePropertyCounts *temporaryPropertyCounts + symbolSortKeys []symbolSortKey mergedSymbols map[*ast.Symbol]*ast.Symbol mergedExportsChecked collections.Set[*ast.Symbol] factory ast.NodeFactory @@ -13695,11 +13696,16 @@ func (c *Checker) getSpreadType(left *Type, right *Type, symbol *ast.Symbol, obj } else { indexInfos = c.getUnionIndexInfos([]*Type{left, right}) } + // Both property lists are usually sorted already, so keep the members in + // their order (left properties first) to make sorting them cheap. + var leftOrder, rightOrder []*ast.Symbol for _, rightProp := range c.getPropertiesOfType(right) { if getDeclarationModifierFlagsFromSymbol(rightProp)&(ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected) != 0 { skippedPrivateMembers.Add(rightProp.Name()) } else if c.isSpreadableProperty(rightProp) { - members[rightProp.Name()] = c.getSpreadSymbol(rightProp, readonly) + spreadProp := c.getSpreadSymbol(rightProp, readonly) + members[rightProp.Name()] = spreadProp + rightOrder = append(rightOrder, spreadProp) } } @@ -13732,15 +13738,25 @@ func (c *Checker) getSpreadType(left *Type, right *Type, symbol *ast.Symbol, obj result.SetDeclarations(declarations) links.setNameType(c, c.valueSymbolLinks.Get(leftProp).getNameType()) members[leftProp.Name()] = result + leftOrder = append(leftOrder, result) } } else { - members[leftProp.Name()] = c.getSpreadSymbol(leftProp, readonly) + spreadProp := c.getSpreadSymbol(leftProp, readonly) + members[leftProp.Name()] = spreadProp + leftOrder = append(leftOrder, spreadProp) + } + } + order := leftOrder + for _, prop := range rightOrder { + if members[prop.Name()] == prop { + order = append(order, prop) } } spreadIndexInfos := core.SameMap(indexInfos, func(info *IndexInfo) *IndexInfo { return c.getIndexInfoWithReadonly(info, readonly) }) - spread := c.newAnonymousType(symbol, members, nil, nil, spreadIndexInfos) + spread := c.newObjectType(ObjectFlagsAnonymous, symbol) + c.setStructuredTypeMembersInOrder(spread, members, order, nil, nil, spreadIndexInfos) spread.objectFlags |= ObjectFlagsObjectLiteral | ObjectFlagsContainsObjectOrArrayLiteral | ObjectFlagsContainsSpread | objectFlags return spread } @@ -22544,9 +22560,29 @@ func (c *Checker) getDefaultOrUnknownFromTypeParameter(t *Type) *Type { } func (c *Checker) getNamedMembers(members ast.SymbolTable, container *ast.Symbol) []*ast.Symbol { + return c.getNamedMembersInOrder(members, nil, container) +} + +// getNamedMembersInOrder is getNamedMembers where order, if not nil, holds the +// symbols of members keyed by their names. The result does not depend on the +// order, but sorting is much cheaper when order is mostly sorted already. +func (c *Checker) getNamedMembersInOrder(members ast.SymbolTable, order []*ast.Symbol, container *ast.Symbol) []*ast.Symbol { if len(members) == 0 { return nil } + if order != nil { + debug.Assert(len(order) == len(members)) + if container == nil || container.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) == 0 { + result := make([]*ast.Symbol, 0, len(order)) + for _, symbol := range order { + if c.isNamedMember(symbol, symbol.Name()) { + result = append(result, symbol) + } + } + c.sortSymbols(result) + return result + } + } // For classes and interfaces, we store explicitly declared members ahead of inherited members. This ensures we process // explicitly declared members first in type relations, which is beneficial because explicitly declared members are more // likely to contain discriminating differences. See for example https://github.com/microsoft/TypeScript/tsc/issues/1968. @@ -25723,10 +25759,14 @@ func (c *Checker) cloneTypeReference(source *Type) *Type { } func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { + c.setStructuredTypeMembersInOrder(t, members, nil, callSignatures, constructSignatures, indexInfos) +} + +func (c *Checker) setStructuredTypeMembersInOrder(t *Type, members ast.SymbolTable, order []*ast.Symbol, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { t.objectFlags |= ObjectFlagsMembersResolved data := t.AsStructuredType() data.members = members - data.properties = c.getNamedMembers(members, t.symbol) + data.properties = c.getNamedMembersInOrder(members, order, t.symbol) c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos) } diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..9bd12e455e09e 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -2,6 +2,8 @@ package checker import ( "encoding/binary" + "fmt" + "math/rand/v2" "runtime" "slices" "strconv" @@ -10,7 +12,11 @@ import ( "unsafe" "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/binder" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/parser" "github.com/microsoft/TypeScript/tsc/internal/testutil/race" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/zeebo/xxh3" "gotest.tools/v3/assert" ) @@ -345,6 +351,45 @@ func TestTemporaryPropertyCounts(t *testing.T) { assert.Assert(t, other.getTemporaryPropertyCounts() != outer) } +func TestSortSymbolsMatchesCompareSymbols(t *testing.T) { + t.Parallel() + c := &Checker{fileIndexMap: make(map[*ast.SourceFile]int)} + c.compareSymbols = c.compareSymbolsWorker + var symbols []*ast.Symbol + for i, name := range []string{"/b.ts", "/a.ts"} { + var text strings.Builder + text.WriteString("interface I {") + for j := range 40 { + fmt.Fprintf(&text, " p%d: any;", (j*7)%40) + } + text.WriteString(" }") + file := parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: tspath.RootedFilePathFromAbsolute(name), PathKey: tspath.PathKeyFromCanonical(name)}, text.String(), core.ScriptKindTS) + binder.BindSourceFile(file) + c.fileIndexMap[file] = i + for _, member := range file.Locals["I"].Members() { + symbols = append(symbols, member) + } + } + for i := range 6 { + symbols = append(symbols, c.newSymbol(ast.SymbolFlagsProperty, fmt.Sprintf("x%d", i%3))) + } + expected := slices.Clone(symbols) + slices.SortFunc(expected, c.compareSymbols) + half := slices.Clone(expected[:len(expected)/2]) + twoRuns := append(slices.Clone(expected[len(expected)/2:]), half...) + inputs := [][]*ast.Symbol{slices.Clone(expected), twoRuns} + rng := rand.New(rand.NewPCG(1, 2)) + for range 20 { + shuffled := slices.Clone(expected) + rng.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) + inputs = append(inputs, shuffled) + } + for _, input := range inputs { + c.sortSymbols(input) + assert.Assert(t, slices.Equal(input, expected)) + } +} + func TestIntersectionReductionReentrancy(t *testing.T) { t.Parallel() c, _ := newCompositeTestChecker() diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 3a7e9993ba879..6d591be688762 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -359,8 +359,116 @@ func createSymbolTable(symbols []*ast.Symbol) ast.SymbolTable { return result } +type symbolSortKey struct { + symbol *ast.Symbol + file *ast.SourceFile + fileIndex int + pos int +} + +// sortSymbols sorts symbols in compareSymbols order. Comparing declarations +// finds each node's source file by walking its parents, so the keys are +// computed once per symbol rather than once per comparison. func (c *Checker) sortSymbols(symbols []*ast.Symbol) { - slices.SortFunc(symbols, c.compareSymbols) + if len(symbols) < 2 { + return + } + keys := c.symbolSortKeys[:0] + for _, s := range symbols { + key := symbolSortKey{symbol: s, fileIndex: -1} + if s != nil { + if decls := s.Declarations(); len(decls) != 0 && decls[0] != nil { + key.file = ast.GetSourceFileOfNode(decls[0]) + key.fileIndex = c.fileIndexMap[key.file] + key.pos = decls[0].Pos() + } + } + keys = append(keys, key) + } + // Symbol lists are often already sorted, or two sorted runs. + descents, split := 0, 0 + for i := 1; i < len(keys) && descents < 2; i++ { + if c.compareSymbolSortKeys(keys[i-1], keys[i]) > 0 { + descents++ + split = i + } + } + switch descents { + case 0: + case 1: + // The keys hold the symbols, so the runs merge straight into symbols. + left, right := keys[:split], keys[split:] + out := symbols[:0] + for len(left) != 0 && len(right) != 0 { + if c.compareSymbolSortKeys(left[0], right[0]) <= 0 { + out = append(out, left[0].symbol) + left = left[1:] + } else { + out = append(out, right[0].symbol) + right = right[1:] + } + } + for _, k := range left { + out = append(out, k.symbol) + } + for _, k := range right { + out = append(out, k.symbol) + } + default: + slices.SortFunc(keys, c.compareSymbolSortKeys) + for i := range keys { + symbols[i] = keys[i].symbol + } + } + clear(keys) + if cap(keys) <= 4096 { + c.symbolSortKeys = keys[:0] + } +} + +// compareSymbolSortKeys matches compareSymbolsWorker. +func (c *Checker) compareSymbolSortKeys(k1, k2 symbolSortKey) int { + s1, s2 := k1.symbol, k2.symbol + if s1 == s2 { + return 0 + } + if s1 == nil { + return 1 + } + if s2 == nil { + return -1 + } + if len(s1.Declarations()) != 0 && len(s2.Declarations()) != 0 { + if r := compareSymbolSortKeyDeclarations(k1, k2); r != 0 { + return r + } + } else if len(s1.Declarations()) != 0 { + return -1 + } else if len(s2.Declarations()) != 0 { + return 1 + } + if r := strings.Compare(s1.Name(), s2.Name()); r != 0 { + return r + } + return cmp.Compare(ast.GetSymbolId(s1), ast.GetSymbolId(s2)) +} + +// compareSymbolSortKeyDeclarations matches compareNodes on the first declarations. +func compareSymbolSortKeyDeclarations(k1, k2 symbolSortKey) int { + d1, d2 := k1.symbol.Declarations()[0], k2.symbol.Declarations()[0] + if d1 == d2 { + return 0 + } + if d1 == nil { + return 1 + } + if d2 == nil { + return -1 + } + if k1.file != k2.file { + return k1.fileIndex - k2.fileIndex + } + return k1.pos - k2.pos } func (c *Checker) compareSymbolsWorker(s1, s2 *ast.Symbol) int {