From 1ede3a0f4f4279ba4e8f70e64ec305487f074eb7 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:02:10 -0700 Subject: [PATCH 1/4] Size Filter and Deduplicate results up front When Filter, FilterIndex, or Deduplicate find the first element to drop, they cloned the kept prefix and then grew the result with append. The clone is sized to the prefix only, so any further kept elements trigger one or more reallocations and copies, and the leftover capacity of each abandoned backing array is wasted. The number of remaining elements bounds the result, so allocate that capacity once. For Filter, first skip past any further removed elements: this makes the bound tighter, and when nothing is kept the result is a zero-capacity slice that needs no allocation at all. The case where nothing is removed still returns the input unchanged. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/core/core.go | 41 ++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/tsc/internal/core/core.go b/tsc/internal/core/core.go index f5c296b9b5bb0..c5f25d5caa6c8 100644 --- a/tsc/internal/core/core.go +++ b/tsc/internal/core/core.go @@ -36,11 +36,21 @@ func ApplyDebugStackLimit() { func Filter[T any](slice []T, f func(T) bool) []T { for i, value := range slice { if !f(value) { - result := slices.Clone(slice[:i]) - for i++; i < len(slice); i++ { - value = slice[i] - if f(value) { - result = append(result, value) + // Find the next kept element so the result can be sized to an upper bound + // up front, and nothing is allocated if no elements are kept. + j := i + 1 + for j < len(slice) && !f(slice[j]) { + j++ + } + result := make([]T, i, i+len(slice)-j) + copy(result, slice[:i]) + if j < len(slice) { + result = append(result, slice[j]) + for j++; j < len(slice); j++ { + value = slice[j] + if f(value) { + result = append(result, value) + } } } return result @@ -64,11 +74,19 @@ func FilterSeq[T any](slice []T, f func(T) bool) iter.Seq[T] { func FilterIndex[T any](slice []T, f func(T, int, []T) bool) []T { for i, value := range slice { if !f(value, i, slice) { - result := slices.Clone(slice[:i]) - for i++; i < len(slice); i++ { - value = slice[i] - if f(value, i, slice) { - result = append(result, value) + j := i + 1 + for j < len(slice) && !f(slice[j], j, slice) { + j++ + } + result := make([]T, i, i+len(slice)-j) + copy(result, slice[:i]) + if j < len(slice) { + result = append(result, slice[j]) + for j++; j < len(slice); j++ { + value = slice[j] + if f(value, j, slice) { + result = append(result, value) + } } } return result @@ -825,7 +843,8 @@ func Deduplicate[T comparable](slice []T) []T { if len(slice) > 1 { for i, value := range slice { if slices.Contains(slice[:i], value) { - result := slices.Clone(slice[:i]) + result := make([]T, i, len(slice)-1) + copy(result, slice[:i]) for i++; i < len(slice); i++ { value = slice[i] if !slices.Contains(result, value) { From 5a346bd25a59437d4f827da7116f7b98925f9c02 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:05:15 -0700 Subject: [PATCH 2/4] Avoid allocating filtered lists in filterType and removeType filterType used core.Filter to build the filtered constituent lists of a union and its origin. Those lists are only inputs to union construction, which interns the result and copies the list only when a new union type has to be created; often an existing union is found and the freshly allocated lists are garbage immediately. Scan for the first removed constituent before doing any work, and build both lists in borrowed temporary type sets instead of on the heap. The origin filter also no longer needs a closure wrapping the predicate. removeType on a two-constituent union (such as `T | undefined`, which is very common) now compares the two constituents directly instead of binary searching with CompareTypes. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 39 ++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index d9cb44e00a903..2db6def4ef07c 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -27311,10 +27311,21 @@ func everyContainedType(t *Type, f func(*Type) bool) bool { func (c *Checker) filterType(t *Type, f func(*Type) bool) *Type { if t.flags&TypeFlagsUnion != 0 { types := t.Types() - filtered := core.Filter(types, f) - if core.Same(types, filtered) { + firstRemoved := slices.IndexFunc(types, func(u *Type) bool { return !f(u) }) + if firstRemoved < 0 { return t } + // The filtered lists are only needed while constructing the result, which copies them + // into a new type if needed, so borrow temporary storage for them. + set := c.getTemporaryTypeSet() + defer c.releaseTemporaryTypeSet(set) + filtered := append(set.values[:0], types[:firstRemoved]...) + for _, u := range types[firstRemoved+1:] { + if f(u) { + filtered = append(filtered, u) + } + } + set.values = filtered origin := t.AsUnionType().origin var newOrigin *Type if origin != nil && origin.flags&TypeFlagsUnion != 0 { @@ -27324,9 +27335,15 @@ func (c *Checker) filterType(t *Type, f func(*Type) bool) *Type { // Otherwise, if we have exactly one type left in the origin set, return that as the filtered type. // Otherwise, construct a new filtered origin type. originTypes := origin.Types() - originFiltered := core.Filter(originTypes, func(u *Type) bool { - return u.flags&TypeFlagsUnion != 0 || f(u) - }) + originSet := c.getTemporaryTypeSet() + defer c.releaseTemporaryTypeSet(originSet) + originFiltered := originSet.values[:0] + for _, u := range originTypes { + if u.flags&TypeFlagsUnion != 0 || f(u) { + originFiltered = append(originFiltered, u) + } + } + originSet.values = originFiltered if len(originTypes)-len(originFiltered) == len(types)-len(filtered) { if len(originFiltered) == 1 { return originFiltered[0] @@ -27355,10 +27372,16 @@ func (c *Checker) removeType(t *Type, targetType *Type) *Type { return c.filterType(t, func(t *Type) bool { return t != targetType }) } types := t.Types() - if i, ok := slices.BinarySearchFunc(types, targetType, CompareTypes); ok { - if len(types) == 2 { - return types[1-i] + if len(types) == 2 { + switch targetType { + case types[0]: + return types[1] + case types[1]: + return types[0] } + return t + } + if i, ok := slices.BinarySearchFunc(types, targetType, CompareTypes); ok { // Remove the target type from the slice. set := c.getTemporaryTypeSet() defer c.releaseTemporaryTypeSet(set) From 9c4777605ccb986f6fc492a5ec54b4335ee398dd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:08:17 -0700 Subject: [PATCH 3/4] Make containsMissingType a plain method containsMissingType was stored as a func-valued checker field built from a closure at checker creation. Calls through the field are indirect and cannot be inlined, and the field costs a word per checker. Nothing needs it to be a value, so make it an ordinary method. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/checker.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 2db6def4ef07c..e953f3039d471 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -913,7 +913,6 @@ type Checker struct { syncIterationTypesResolver *IterationTypesResolver asyncIterationTypesResolver *IterationTypesResolver isPrimitiveOrObjectOrEmptyType func(*Type) bool - containsMissingType func(*Type) bool couldContainTypeVariables func(*Type) bool isStringIndexSignatureOnlyType func(*Type) bool markNodeAssignments func(*ast.Node) bool @@ -1288,15 +1287,16 @@ func (c *Checker) initializeClosures() { c.isPrimitiveOrObjectOrEmptyType = func(t *Type) bool { return t.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.IsEmptyAnonymousObjectType(t) } - c.containsMissingType = func(t *Type) bool { - return t == c.missingType || t.flags&TypeFlagsUnion != 0 && t.Types()[0] == c.missingType - } c.couldContainTypeVariables = c.couldContainTypeVariablesWorker c.isStringIndexSignatureOnlyType = c.isStringIndexSignatureOnlyTypeWorker c.markNodeAssignments = c.markNodeAssignmentsWorker c.compareTypesAssignable = c.compareTypesAssignableWorker } +func (c *Checker) containsMissingType(t *Type) bool { + return t == c.missingType || t.flags&TypeFlagsUnion != 0 && t.Types()[0] == c.missingType +} + func (c *Checker) initializeIterationResolvers() { c.syncIterationTypesResolver = &IterationTypesResolver{ iteratorSymbolName: "iterator", From 9fe6af677cc7cf5e913f646cfd91566d95c086db Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:09:40 -0700 Subject: [PATCH 4/4] Hoist relateVariances out of structuredTypeRelatedToWorker structuredTypeRelatedToWorker is one of the hottest functions in the checker, but it built the relateVariances closure on every call, even though only the generic type reference and alias paths use it. The closure also captured and assigned several of the function's locals, which forces them to live in memory for the whole body instead of in registers. Make it a method that receives the state it updates explicitly. The closure also assigned the outer result, but on every path where the caller continues that value is TernaryFalse, which result already holds there. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/checker/relater.go | 91 +++++++++++++++++---------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 562ae0451cb43..61f405dd5fe35 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -3313,53 +3313,56 @@ func (r *Relater) isSourceIntersectionNeedingExtraCheck(source *Type, target *Ty }) } +// relateVariances relates type arguments according to the given variances. The boolean result +// reports whether the returned Ternary is final; otherwise a structural comparison should follow. +func (r *Relater) relateVariances(sourceTypeArguments []*Type, targetTypeArguments []*Type, variances []VarianceFlags, reportErrors bool, intersectionState IntersectionState, saveErrorState errorState, varianceCheckFailed *bool, originalErrorChain **ErrorChain) (Ternary, bool) { + if result := r.typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState); result != TernaryFalse { + return result, true + } + if core.Some(variances, func(v VarianceFlags) bool { return v&VarianceFlagsAllowsStructuralFallback != 0 }) { + // If some type parameter was `Unmeasurable` or `Unreliable`, and we couldn't pass by assuming it was identical, then we + // have to allow a structural fallback check + // We elide the variance-based error elaborations, since those might not be too helpful, since we'll potentially + // be assuming identity of the type parameter. + *originalErrorChain = nil + r.restoreErrorState(saveErrorState) + return TernaryFalse, false + } + allowStructuralFallback := r.c.hasCovariantVoidArgument(targetTypeArguments, variances) + *varianceCheckFailed = !allowStructuralFallback + // The type arguments did not relate appropriately, but it may be because we have no variance + // information (in which case typeArgumentsRelatedTo defaulted to covariance for all type + // arguments). It might also be the case that the target type has a 'void' type argument for + // a covariant type parameter that is only used in return positions within the generic type + // (in which case any type argument is permitted on the source side). In those cases we proceed + // with a structural comparison. Otherwise, we know for certain the instantiations aren't + // related and we can return here. + if len(variances) != 0 && !allowStructuralFallback { + // In some cases generic types that are covariant in regular type checking mode become + // invariant in --strictFunctionTypes mode because one or more type parameters are used in + // both co- and contravariant positions. In order to make it easier to diagnose *why* such + // types are invariant, if any of the type parameters are invariant we reset the reported + // errors and instead force a structural comparison (which will include elaborations that + // reveal the reason). + // We can switch on `reportErrors` here, since varianceCheckFailed guarantees we return `False`, + // we can return `False` early here to skip calculating the structural error message we don't need. + if *varianceCheckFailed && !(reportErrors && core.Some(variances, func(v VarianceFlags) bool { return (v & VarianceFlagsVarianceMask) == VarianceFlagsInvariant })) { + return TernaryFalse, true + } + // We remember the original error information so we can restore it in case the structural + // comparison unexpectedly succeeds. This can happen when the structural comparison result + // is a Ternary.Maybe for example caused by the recursion depth limiter. + *originalErrorChain = r.errorChain + r.restoreErrorState(saveErrorState) + } + return TernaryFalse, false +} + func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, reportErrors bool, intersectionState IntersectionState) Ternary { var result Ternary var varianceCheckFailed bool var originalErrorChain *ErrorChain saveErrorState := r.getErrorState() - relateVariances := func(sourceTypeArguments []*Type, targetTypeArguments []*Type, variances []VarianceFlags, intersectionState IntersectionState) (Ternary, bool) { - if result = r.typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState); result != TernaryFalse { - return result, true - } - if core.Some(variances, func(v VarianceFlags) bool { return v&VarianceFlagsAllowsStructuralFallback != 0 }) { - // If some type parameter was `Unmeasurable` or `Unreliable`, and we couldn't pass by assuming it was identical, then we - // have to allow a structural fallback check - // We elide the variance-based error elaborations, since those might not be too helpful, since we'll potentially - // be assuming identity of the type parameter. - originalErrorChain = nil - r.restoreErrorState(saveErrorState) - return TernaryFalse, false - } - allowStructuralFallback := r.c.hasCovariantVoidArgument(targetTypeArguments, variances) - varianceCheckFailed = !allowStructuralFallback - // The type arguments did not relate appropriately, but it may be because we have no variance - // information (in which case typeArgumentsRelatedTo defaulted to covariance for all type - // arguments). It might also be the case that the target type has a 'void' type argument for - // a covariant type parameter that is only used in return positions within the generic type - // (in which case any type argument is permitted on the source side). In those cases we proceed - // with a structural comparison. Otherwise, we know for certain the instantiations aren't - // related and we can return here. - if len(variances) != 0 && !allowStructuralFallback { - // In some cases generic types that are covariant in regular type checking mode become - // invariant in --strictFunctionTypes mode because one or more type parameters are used in - // both co- and contravariant positions. In order to make it easier to diagnose *why* such - // types are invariant, if any of the type parameters are invariant we reset the reported - // errors and instead force a structural comparison (which will include elaborations that - // reveal the reason). - // We can switch on `reportErrors` here, since varianceCheckFailed guarantees we return `False`, - // we can return `False` early here to skip calculating the structural error message we don't need. - if varianceCheckFailed && !(reportErrors && core.Some(variances, func(v VarianceFlags) bool { return (v & VarianceFlagsVarianceMask) == VarianceFlagsInvariant })) { - return TernaryFalse, true - } - // We remember the original error information so we can restore it in case the structural - // comparison unexpectedly succeeds. This can happen when the structural comparison result - // is a Ternary.Maybe for example caused by the recursion depth limiter. - originalErrorChain = r.errorChain - r.restoreErrorState(saveErrorState) - } - return TernaryFalse, false - } switch { case r.relation == r.c.identityRelation: // We've already checked that source.flags and target.flags are identical @@ -3455,7 +3458,7 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo nodeIsInJsFile := ast.IsInJSFile(source.alias.symbol.ValueDeclaration()) sourceTypes := r.c.fillMissingTypeArguments(source.alias.typeArguments, params, minParams, nodeIsInJsFile) targetTypes := r.c.fillMissingTypeArguments(target.alias.typeArguments, params, minParams, nodeIsInJsFile) - varianceResult, ok := relateVariances(sourceTypes, targetTypes, variances, intersectionState) + varianceResult, ok := r.relateVariances(sourceTypes, targetTypes, variances, reportErrors, intersectionState, saveErrorState, &varianceCheckFailed, &originalErrorChain) if ok { return varianceResult } @@ -3889,7 +3892,7 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo if len(variances) == 0 { return TernaryUnknown } - varianceResult, ok := relateVariances(r.c.getTypeArguments(source), r.c.getTypeArguments(target), variances, intersectionState) + varianceResult, ok := r.relateVariances(r.c.getTypeArguments(source), r.c.getTypeArguments(target), variances, reportErrors, intersectionState, saveErrorState, &varianceCheckFailed, &originalErrorChain) if ok { return varianceResult }