Skip to content

Commit

Permalink
Only cache union/intersection relations once
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg committed Feb 5, 2022
1 parent b82966f commit f0b8742
Showing 1 changed file with 68 additions and 71 deletions.
139 changes: 68 additions & 71 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit f0b8742

Please sign in to comment.