Skip to content

Cache results of expensive repetitive type operations #49760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 5, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 45 additions & 23 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ namespace ts {
const stringMappingTypes = new Map<string, StringMappingType>();
const substitutionTypes = new Map<string, SubstitutionType>();
const subtypeReductionCache = new Map<string, Type[]>();
const cachedTypes = new Map<string, Type>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why one cache for multiple calls rather than one per? I would have thought that would be more efficient because you don't have to make special keys (and could just use the type ids as keys) and for locality reasons, so I'm curious is there's some perf thing I'm missing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In two of the three cases we have to create composite keys anyways, so just by making sure the keys don't overlap we can share the same cache and access helpers. In the third case we could just go by type id, but would save us very little time and could add more lines of code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed the composite key.

const evolvingArrayTypes: EvolvingArrayType[] = [];
const undefinedProperties: SymbolTable = new Map();
const markerTypes = new Set<number>();
Expand Down Expand Up @@ -1061,6 +1062,15 @@ namespace ts {

return checker;

function getCachedType(key: string | undefined) {
return key ? cachedTypes.get(key) : undefined;
}

function setCachedType(key: string | undefined, type: Type) {
if (key) cachedTypes.set(key, type);
return type;
}

function getJsxNamespace(location: Node | undefined): __String {
if (location) {
const file = getSourceFileOfNode(location);
Expand Down Expand Up @@ -21295,10 +21305,15 @@ namespace ts {
type.flags & TypeFlags.NumberLiteral ? numberType :
type.flags & TypeFlags.BigIntLiteral ? bigintType :
type.flags & TypeFlags.BooleanLiteral ? booleanType :
type.flags & TypeFlags.Union ? mapType(type as UnionType, getBaseTypeOfLiteralType) :
type.flags & TypeFlags.Union ? getBaseTypeOfLiteralTypeUnion(type as UnionType) :
type;
}

function getBaseTypeOfLiteralTypeUnion(type: UnionType) {
const key = `B${getTypeId(type)}`;
return getCachedType(key) ?? setCachedType(key, mapType(type, getBaseTypeOfLiteralType));
}

function getWidenedLiteralType(type: Type): Type {
return type.flags & TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(type as LiteralType) :
type.flags & TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType :
Expand Down Expand Up @@ -23531,23 +23546,25 @@ namespace ts {
// For example, when a variable of type number | string | boolean is assigned a value of type number | boolean,
// we remove type string.
function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) {
if (declaredType !== assignedType) {
if (assignedType.flags & TypeFlags.Never) {
return assignedType;
}
let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
if (assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType)) {
reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types
}
// Our crude heuristic produces an invalid result in some cases: see GH#26130.
// For now, when that happens, we give up and don't narrow at all. (This also
// means we'll never narrow for erroneous assignments where the assigned type
// is not assignable to the declared type.)
if (isTypeAssignableTo(assignedType, reducedType)) {
return reducedType;
}
if (declaredType === assignedType) {
return declaredType;
}
return declaredType;
if (assignedType.flags & TypeFlags.Never) {
return assignedType;
}
const key = `A${getTypeId(declaredType)},${getTypeId(assignedType)}`;
return getCachedType(key) ?? setCachedType(key, getAssignmentReducedTypeWorker(declaredType, assignedType));
}

function getAssignmentReducedTypeWorker(declaredType: UnionType, assignedType: Type) {
const filteredType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
// Ensure that we narrow to fresh types if the assignment is a fresh boolean literal type.
const reducedType = assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType) ? mapType(filteredType, getFreshTypeOfLiteralType) : filteredType;
// Our crude heuristic produces an invalid result in some cases: see GH#26130.
// For now, when that happens, we give up and don't narrow at all. (This also
// means we'll never narrow for erroneous assignments where the assigned type
// is not assignable to the declared type.)
return isTypeAssignableTo(assignedType, reducedType) ? reducedType : declaredType;
}

function isFunctionObjectType(type: ObjectType): boolean {
Expand Down Expand Up @@ -25041,7 +25058,7 @@ namespace ts {
const targetType = hasStaticModifier(Debug.checkDefined(symbol.valueDeclaration, "should always have a declaration"))
? getTypeOfSymbol(classSymbol) as InterfaceType
: getDeclaredTypeOfSymbol(classSymbol);
return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom);
return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true);
}

function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
Expand Down Expand Up @@ -25315,10 +25332,16 @@ namespace ts {
if (!nonConstructorTypeInUnion) return type;
}

return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom);
return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true);
}

function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0)}` : undefined;
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived));
}

function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, isRelated: (source: Type, target: Type) => boolean) {
function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
if (!assumeTrue) {
return filterType(type, t => !isRelated(t, candidate));
}
Expand All @@ -25330,7 +25353,6 @@ namespace ts {
return assignableType;
}
}

// If the candidate type is a subtype of the target type, narrow to the candidate type.
// Otherwise, if the target type is assignable to the candidate type, keep the target type.
// Otherwise, if the candidate type is assignable to the target type, narrow to the candidate
Expand Down Expand Up @@ -25369,15 +25391,15 @@ namespace ts {
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
if (predicateArgument) {
if (isMatchingReference(reference, predicateArgument)) {
return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf);
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false);
}
if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) &&
!(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) {
type = getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
}
const access = getDiscriminantPropertyAccess(predicateArgument, type);
if (access) {
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf));
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false));
}
}
}
Expand Down