From f0b87421ed6141bc2a42c74b122be38d942098ce Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 5 Feb 2022 09:23:54 -0800 Subject: [PATCH] Only cache union/intersection relations once --- src/compiler/checker.ts | 139 ++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e8c1ba5e93a5c..e37c68f10b1eb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -18346,30 +18346,17 @@ namespace ts { let result = Ternary.False; const saveErrorInfo = captureErrorCalculationState(); - if (source.flags & TypeFlags.UnionOrIntersection || target.flags & TypeFlags.UnionOrIntersection) { - // We skip caching when source or target is a union with no more than three constituents. - result = (source.flags & TypeFlags.Union || target.flags & TypeFlags.Union) && getConstituentCount(source) * getConstituentCount(target) < 4 ? - structuredTypeRelatedTo(source, target, reportErrors, intersectionState | IntersectionState.UnionIntersectionCheck) : - recursiveTypeRelatedTo(source, target, reportErrors, intersectionState | IntersectionState.UnionIntersectionCheck, recursionFlags); - // The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle: - // Source is instantiable (e.g. source has union or intersection constraint). - // Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }). - // Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }). - // Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }). - // Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }). - if (!result && (source.flags & TypeFlags.Instantiable || - source.flags & TypeFlags.Object && target.flags & TypeFlags.Union || - source.flags & TypeFlags.Intersection && target.flags & (TypeFlags.Object | TypeFlags.Union | TypeFlags.Instantiable))) { - if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) { - resetErrorInfo(saveErrorInfo); - } + if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) { + const skipCaching = source.flags & TypeFlags.Union && (source as UnionType).types.length < 4 && !(target.flags & TypeFlags.Union) || + target.flags & TypeFlags.Union && (target as UnionType).types.length < 4 && !(source.flags & TypeFlags.StructuredOrInstantiable); + if (skipCaching) { + result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState); } - } - else if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) { - if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) { + else if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) { resetErrorInfo(saveErrorInfo); } } + if (!result && source.flags & (TypeFlags.Intersection | TypeFlags.TypeParameter)) { // The combined constraint of an intersection type is the intersection of the constraints of // the constituents. When an intersection type contains instantiable types with union type @@ -18606,6 +18593,51 @@ namespace ts { return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration; } + function unionOrIntersectionRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + // Note that these checks are specifically ordered to produce correct results. In particular, + // we need to deconstruct unions before intersections (because unions are always at the top), + // and we need to handle "each" relations before "some" relations for the same kind of type. + if (source.flags & TypeFlags.Union) { + return relation === comparableRelation ? + someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck) : + eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck); + } + if (target.flags & TypeFlags.Union) { + return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as UnionType, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive)); + } + if (target.flags & TypeFlags.Intersection) { + return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target); + } + // Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the + // constraints of all non-primitive types in the source into a new intersection. We do this because the + // intersection may further constrain the constraints of the non-primitive types. For example, given a type + // parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't + // appear to be comparable to '2'. + if (relation === comparableRelation && target.flags & TypeFlags.Primitive) { + const constraints = sameMap((source as IntersectionType).types, getBaseConstraintOrType); + if (constraints !== (source as IntersectionType).types) { + source = getIntersectionType(constraints); + if (!(source.flags & TypeFlags.Intersection)) { + return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false); + } + } + } + // Check to see if any constituents of the intersection are immediately related to the target. + // + // Don't report errors though. Checking whether a constituent is related to the source is not actually + // useful and leads to some confusing error messages. Instead it is better to let the below checks + // take care of this, or to not elaborate at all. For instance, + // + // - For an object type (such as 'C = A & B'), users are usually more interested in structural errors. + // + // - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection + // than to report that 'D' is not assignable to 'A' or 'B'. + // + // - For a primitive type or type parameter (such as 'number = A & B') there is no point in + // breaking the intersection apart. + return someTypeRelatedToType(source as IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source); + } + function eachTypeRelatedToSomeType(source: UnionOrIntersectionType, target: UnionOrIntersectionType): Ternary { let result = Ternary.True; const sourceTypes = source.types; @@ -18903,49 +18935,23 @@ namespace ts { if (intersectionState & IntersectionState.PropertyCheck) { return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); } - if (intersectionState & IntersectionState.UnionIntersectionCheck) { - // Note that these checks are specifically ordered to produce correct results. In particular, - // we need to deconstruct unions before intersections (because unions are always at the top), - // and we need to handle "each" relations before "some" relations for the same kind of type. - if (source.flags & TypeFlags.Union) { - return relation === comparableRelation ? - someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck) : - eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck); - } - if (target.flags & TypeFlags.Union) { - return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as UnionType, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive)); - } - if (target.flags & TypeFlags.Intersection) { - return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target); - } - // Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the - // constraints of all non-primitive types in the source into a new intersection. We do this because the - // intersection may further constrain the constraints of the non-primitive types. For example, given a type - // parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't - // appear to be comparable to '2'. - if (relation === comparableRelation && target.flags & TypeFlags.Primitive) { - const constraints = sameMap((source as IntersectionType).types, getBaseConstraintOrType); - if (constraints !== (source as IntersectionType).types) { - source = getIntersectionType(constraints); - if (!(source.flags & TypeFlags.Intersection)) { - return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false); - } - } + let result: Ternary; + let originalErrorInfo: DiagnosticMessageChain | undefined; + let varianceCheckFailed = false; + const saveErrorInfo = captureErrorCalculationState(); + if (source.flags & TypeFlags.UnionOrIntersection || target.flags & TypeFlags.UnionOrIntersection) { + result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState); + // The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle: + // Source is instantiable (e.g. source has union or intersection constraint). + // Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }). + // Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }). + // Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }). + // Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }). + if (result || !(source.flags & TypeFlags.Instantiable || + source.flags & TypeFlags.Object && target.flags & TypeFlags.Union || + source.flags & TypeFlags.Intersection && target.flags & (TypeFlags.Object | TypeFlags.Union | TypeFlags.Instantiable))) { + return result; } - // Check to see if any constituents of the intersection are immediately related to the target. - // - // Don't report errors though. Checking whether a constituent is related to the source is not actually - // useful and leads to some confusing error messages. Instead it is better to let the below checks - // take care of this, or to not elaborate at all. For instance, - // - // - For an object type (such as 'C = A & B'), users are usually more interested in structural errors. - // - // - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection - // than to report that 'D' is not assignable to 'A' or 'B'. - // - // - For a primitive type or type parameter (such as 'number = A & B') there is no point in - // breaking the intersection apart. - return someTypeRelatedToType(source as IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source); } const flags = source.flags & target.flags; if (relation === identityRelation && !(flags & TypeFlags.Object)) { @@ -18979,11 +18985,6 @@ namespace ts { return Ternary.False; } - let result: Ternary; - let originalErrorInfo: DiagnosticMessageChain | undefined; - let varianceCheckFailed = false; - const saveErrorInfo = captureErrorCalculationState(); - // We limit alias variance probing to only object and conditional types since their alias behavior // is more predictable than other, interned types, which may or may not have an alias depending on // the order in which things were checked. @@ -23338,10 +23339,6 @@ namespace ts { mapType(type, mapper); } - function getConstituentCount(type: Type) { - return type.flags & TypeFlags.Union ? (type as UnionType).types.length : 1; - } - function extractTypesOfKind(type: Type, kind: TypeFlags) { return filterType(type, t => (t.flags & kind) !== 0); }