From 5e29a0a46d4f55f2256ca321842b88acfd9c29c2 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 13 Apr 2023 17:40:45 -0700 Subject: [PATCH 1/4] Optimize union type creation --- src/compiler/checker.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 780b120bb7e5f..cab68bc834e0a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1881,6 +1881,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { var tupleTypes = new Map(); var unionTypes = new Map(); + var unionOfUnionTypes = new Map(); var intersectionTypes = new Map(); var stringLiteralTypes = new Map(); var numberLiteralTypes = new Map(); @@ -16234,9 +16235,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) { const flags = type.flags; - if (flags & TypeFlags.Union) { - return addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types); - } // We ignore 'never' types in unions if (!(flags & TypeFlags.Never)) { includes |= flags & TypeFlags.IncludesMask; @@ -16259,8 +16257,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Add the given types to the given type set. Order is preserved, duplicates are removed, // and nested types of the given kind are flattened into the set. function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: readonly Type[]): TypeFlags { + let lastUnion: Type | undefined; for (const type of types) { - includes = addTypeToUnion(typeSet, includes, type); + if (type.flags & TypeFlags.Union) { + if (type !== lastUnion) { + includes = addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types); + lastUnion = type; + } + } + else { + includes = addTypeToUnion(typeSet, includes, type); + } } return includes; } @@ -16412,6 +16419,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (types.length === 1) { return types[0]; } + if (types.length === 2 && !origin && (types[0].flags & TypeFlags.Union || types[1].flags & TypeFlags.Union)) { + const infix = unionReduction === UnionReduction.None ? "N" : unionReduction === UnionReduction.Subtype ? "S" : "L"; + const id = types[0].id + infix + types[1].id + getAliasId(aliasSymbol, aliasTypeArguments); + let type = unionOfUnionTypes.get(id); + if (!type) { + type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin); + unionOfUnionTypes.set(id, type); + } + return type; + } + return getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin); + } + + function getUnionTypeWorker(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], origin?: Type): Type { let typeSet: Type[] | undefined = []; const includes = addTypesToUnion(typeSet, 0 as TypeFlags, types); if (unionReduction !== UnionReduction.None) { From 46d1c4e274012b4d75adb10140f977e9a581f642 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 13 Apr 2023 17:57:49 -0700 Subject: [PATCH 2/4] Add comments --- src/compiler/checker.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cab68bc834e0a..77041f5b9f3ea 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16260,6 +16260,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let lastUnion: Type | undefined; for (const type of types) { if (type.flags & TypeFlags.Union) { + // We skip the union type if it is the same as the last union we processed. We could potentially track + // all union types that we've processed, but this simple test is fast and covers the scenarios we care + // about (in particular, Record[A], where A and B are large union types). if (type !== lastUnion) { includes = addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types); lastUnion = type; @@ -16419,6 +16422,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (types.length === 1) { return types[0]; } + // We optimize for the common case of unioning a union type with some other type (such as `undefined`). if (types.length === 2 && !origin && (types[0].flags & TypeFlags.Union || types[1].flags & TypeFlags.Union)) { const infix = unionReduction === UnionReduction.None ? "N" : unionReduction === UnionReduction.Subtype ? "S" : "L"; const id = types[0].id + infix + types[1].id + getAliasId(aliasSymbol, aliasTypeArguments); From 14f30d31353c3291f398a2529233c9f209620ab0 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 14 Apr 2023 08:09:08 -0700 Subject: [PATCH 3/4] Sort cache key / cache last type instead of just last union --- src/compiler/checker.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 77041f5b9f3ea..3333cb67ac320 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16257,19 +16257,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Add the given types to the given type set. Order is preserved, duplicates are removed, // and nested types of the given kind are flattened into the set. function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: readonly Type[]): TypeFlags { - let lastUnion: Type | undefined; + let lastType: Type | undefined; for (const type of types) { - if (type.flags & TypeFlags.Union) { - // We skip the union type if it is the same as the last union we processed. We could potentially track - // all union types that we've processed, but this simple test is fast and covers the scenarios we care - // about (in particular, Record[A], where A and B are large union types). - if (type !== lastUnion) { - includes = addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types); - lastUnion = type; - } - } - else { - includes = addTypeToUnion(typeSet, includes, type); + // We skip the type if it is the same as the last type we processed. This simple test particularly + // saves a lot of work for large lists of the same union type, such as when resolving `Record[A]`, + // where A and B are large union types. + if (type !== lastType) { + includes = type.flags & TypeFlags.Union ? + addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types) : + addTypeToUnion(typeSet, includes, type); + lastType = type; } } return includes; @@ -16425,7 +16422,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // We optimize for the common case of unioning a union type with some other type (such as `undefined`). if (types.length === 2 && !origin && (types[0].flags & TypeFlags.Union || types[1].flags & TypeFlags.Union)) { const infix = unionReduction === UnionReduction.None ? "N" : unionReduction === UnionReduction.Subtype ? "S" : "L"; - const id = types[0].id + infix + types[1].id + getAliasId(aliasSymbol, aliasTypeArguments); + const index = types[0].id < types[1].id ? 0 : 1; + const id = types[index].id + infix + types[1 - index].id + getAliasId(aliasSymbol, aliasTypeArguments); let type = unionOfUnionTypes.get(id); if (!type) { type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin); From 10bc75433d11e97997c9fdc0aa94728a457c274c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 14 Apr 2023 09:03:27 -0700 Subject: [PATCH 4/4] Address CR feedback --- src/compiler/checker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3333cb67ac320..053cde0e2e448 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16426,7 +16426,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const id = types[index].id + infix + types[1 - index].id + getAliasId(aliasSymbol, aliasTypeArguments); let type = unionOfUnionTypes.get(id); if (!type) { - type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin); + type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, /*origin*/ undefined); unionOfUnionTypes.set(id, type); } return type; @@ -16434,7 +16434,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin); } - function getUnionTypeWorker(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], origin?: Type): Type { + function getUnionTypeWorker(types: readonly Type[], unionReduction: UnionReduction, aliasSymbol: Symbol | undefined, aliasTypeArguments: readonly Type[] | undefined, origin: Type | undefined): Type { let typeSet: Type[] | undefined = []; const includes = addTypesToUnion(typeSet, 0 as TypeFlags, types); if (unionReduction !== UnionReduction.None) {