Skip to content

Commit 2d21364

Browse files
committed
Attempt to reduce types about to produce a complexity error
1 parent af5e8e2 commit 2d21364

File tree

7 files changed

+785
-12
lines changed

7 files changed

+785
-12
lines changed

src/compiler/checker.ts

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,8 @@ export function isInstantiatedModule(node: ModuleDeclaration, preserveConstEnums
13811381
(preserveConstEnums && moduleState === ModuleInstanceState.ConstEnumOnly);
13821382
}
13831383

1384+
const UNION_CROSS_PRODUCT_SIZE_LIMIT = 100_000;
1385+
13841386
/** @internal */
13851387
export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
13861388
// Why var? It avoids TDZ checks in the runtime which can be costly.
@@ -11451,7 +11453,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1145111453
if (!links.type) {
1145211454
Debug.assertIsDefined(links.deferralParent);
1145311455
Debug.assertIsDefined(links.deferralConstituents);
11454-
links.type = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents);
11456+
const operation = links.deferralParent.flags & TypeFlags.Union ? getUnionType : getIntersectionType;
11457+
let result = operation(links.deferralConstituents);
11458+
for (const part of links.deferredMismatchedParts || emptyArray) {
11459+
result = operation([result, getTypeOfSymbol(part)]);
11460+
}
11461+
links.type = result;
1145511462
}
1145611463
return links.type;
1145711464
}
@@ -11460,8 +11467,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1146011467
const links = getSymbolLinks(symbol);
1146111468
if (!links.writeType && links.deferralWriteConstituents) {
1146211469
Debug.assertIsDefined(links.deferralParent);
11463-
Debug.assertIsDefined(links.deferralConstituents);
11464-
links.writeType = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralWriteConstituents) : getIntersectionType(links.deferralWriteConstituents);
11470+
const operation = links.deferralParent.flags & TypeFlags.Union ? getUnionType : getIntersectionType;
11471+
let result = operation(links.deferralWriteConstituents);
11472+
for (const part of links.deferredMismatchedParts || emptyArray) {
11473+
result = operation([result, getTypeOfSymbol(part)]);
11474+
}
11475+
links.writeType = result;
1146511476
}
1146611477
return links.writeType;
1146711478
}
@@ -14035,8 +14046,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1403514046
let declarations: Declaration[] | undefined;
1403614047
let firstType: Type | undefined;
1403714048
let nameType: Type | undefined;
14038-
const propTypes: Type[] = [];
14049+
let propTypes: Type[] = [];
1403914050
let writeTypes: Type[] | undefined;
14051+
let deferredMismatchedParts: Symbol[] | undefined;
1404014052
let firstValueDeclaration: Declaration | undefined;
1404114053
let hasNonUniformValueDeclaration = false;
1404214054
for (const prop of props) {
@@ -14047,6 +14059,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1404714059
hasNonUniformValueDeclaration = true;
1404814060
}
1404914061
declarations = addRange(declarations, prop.declarations);
14062+
if (getCheckFlags(prop) & CheckFlags.DeferredType) {
14063+
checkFlags |= (getCheckFlags(prop) & (CheckFlags.HasNeverType | CheckFlags.HasNonUniformType | CheckFlags.HasLiteralType));
14064+
if (!nameType) {
14065+
nameType = getSymbolLinks(prop).nameType;
14066+
}
14067+
if ((getSymbolLinks(prop).deferralParent?.flags! & TypeFlags.UnionOrIntersection) === (containingType.flags & TypeFlags.UnionOrIntersection)) {
14068+
// Member has a deferred type (of the same kind) - rather than eagerly resolving it, pass on the deferral
14069+
const deferredWriteTypes = getSymbolLinks(prop).deferralWriteConstituents;
14070+
if (deferredWriteTypes) {
14071+
writeTypes = concatenate(!writeTypes ? propTypes.slice() : writeTypes, deferredWriteTypes);
14072+
}
14073+
propTypes = concatenate(propTypes, getSymbolLinks(prop).deferralConstituents!);
14074+
deferredMismatchedParts = concatenate(deferredMismatchedParts, getSymbolLinks(prop).deferredMismatchedParts);
14075+
break;
14076+
}
14077+
else {
14078+
// deferred union used in an intersection or intersection used within an union - defer the whole construct
14079+
deferredMismatchedParts = append(deferredMismatchedParts, prop);
14080+
break;
14081+
}
14082+
}
1405014083
const type = getTypeOfSymbol(prop);
1405114084
if (!firstType) {
1405214085
firstType = type;
@@ -14081,12 +14114,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1408114114

1408214115
result.declarations = declarations;
1408314116
result.links.nameType = nameType;
14084-
if (propTypes.length > 2) {
14117+
if (propTypes.length > 2 || deferredMismatchedParts) {
1408514118
// When `propTypes` has the potential to explode in size when normalized, defer normalization until absolutely needed
1408614119
result.links.checkFlags |= CheckFlags.DeferredType;
1408714120
result.links.deferralParent = containingType;
1408814121
result.links.deferralConstituents = propTypes;
1408914122
result.links.deferralWriteConstituents = writeTypes;
14123+
result.links.deferredMismatchedParts = deferredMismatchedParts;
1409014124
}
1409114125
else {
1409214126
result.links.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes);
@@ -16723,7 +16757,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1672316757
function getIntersectionType(types: readonly Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], noSupertypeReduction?: boolean): Type {
1672416758
const typeMembershipMap: Map<string, Type> = new Map();
1672516759
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
16726-
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
16760+
let typeSet: Type[] = arrayFrom(typeMembershipMap.values());
1672716761
// An intersection type is considered empty if it contains
1672816762
// the type never, or
1672916763
// more than one unit type or,
@@ -16774,6 +16808,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1677416808
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
1677516809
let result = intersectionTypes.get(id);
1677616810
if (!result) {
16811+
const originalSet = typeSet;
16812+
let runningResult: Type | undefined;
1677716813
if (includes & TypeFlags.Union) {
1677816814
if (intersectUnionsOfPrimitiveTypes(typeSet)) {
1677916815
// When the intersection creates a reduced set (which might mean that *all* union types have
@@ -16791,18 +16827,56 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1679116827
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
1679216828
}
1679316829
else {
16830+
if (typeSet.length > 2 && getCrossProductUnionSize(typeSet) >= UNION_CROSS_PRODUCT_SIZE_LIMIT && every(typeSet, t => !!(t.flags & TypeFlags.Union) || !!(t.flags & TypeFlags.Primitive))) {
16831+
// This type set is going to trigger an "expression too complex" error below. Rather than resort to that, as a last, best effort, simplify the type.
16832+
// When the intersection looks like (A | B | C) & (D | E | F) & (G | H | I) - in the general case, this can result in a massive resulting
16833+
// union, hence the check on the cross product size below, _however_ in some cases we can also _simplify_ the resulting type massively.
16834+
// If we can recognize that upfront, we can still allow the type to form without creating innumerable intermediate types.
16835+
// Specifically, in cases where almost all combinations are known to reduce to `never` (so the result is essentially sparse)
16836+
// and we can recognize that quickly, we can use a simplified result without checking the worst-case size.
16837+
// So we start with the assumption that the result _is_ sparse when the input looks like the above, and we assume the result
16838+
// will take the form (A & D & G) | (B & E & H) | (C & F & I). To validate this, we reduce left, first combining
16839+
// (A | B | C) & (D | E | F); if that combines into `(A & D) | (B & E) | (C & F)` like we want, which we make 9 intermediate
16840+
// types to check, we can then combine the reduced `(A & D) | (B & E) | (C & F)` with (G | H | I), which again takes 9 intermediate types
16841+
// to check, finally producing `(A & D & G) | (B & E & H) | (C & F & I)`. This required 18 intermediate types, while the standard method
16842+
// of expanding (A | B | C) & (D | E | F) & (G | H | I) would produce 27 types and then perform reduction on the result.
16843+
// By going elemnt-wise, and bailing if the result fails to reduce, we can allow these sparse expansions without doing undue work.
16844+
runningResult = typeSet[0];
16845+
for (let i = 1; i < typeSet.length; i++) {
16846+
// For intersection reduction, here we're considering `undefined & (A | B)` as `never`. (ie, we're disallowing branded primitives)
16847+
// This is relevant for, eg, when looking at `(HTMLElement | null) & (SVGElement | null) & ... & undefined` where _usually_
16848+
// we'd allow for tons of garbage intermediate types like `null & SVGElement` to exist; but nobody ever really actually _wants_
16849+
// that, IMO. Those types can still exist in the type system; just... not when working with unions and intersections with massive
16850+
// cross-product growth potential.
16851+
runningResult = typeSet[i].flags & TypeFlags.Primitive && everyType(runningResult, t => !!(t.flags & TypeFlags.Object)) ? neverType : getReducedType(intersectTypes(runningResult, typeSet[i]));
16852+
if (i === typeSet.length - 1 || isTypeAny(runningResult) || runningResult.flags & TypeFlags.Never) {
16853+
return runningResult;
16854+
}
16855+
if (!(runningResult.flags & TypeFlags.Union) || (runningResult as UnionType).types.length > typeSet.length) {
16856+
// Save work done by the accumulated result thus far, even if we're bailing on the heuristic.
16857+
// It may have saved us enough work already that we're willing to work with the type now.
16858+
typeSet = typeSet.slice(i + 1);
16859+
break;
16860+
}
16861+
}
16862+
}
1679416863
// We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of
1679516864
// the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type
1679616865
// exceeds 100000 constituents, report an error.
1679716866
if (!checkCrossProductUnion(typeSet)) {
1679816867
return errorType;
1679916868
}
1680016869
const constituents = getCrossProductIntersections(typeSet);
16801-
// We attach a denormalized origin type when at least one constituent of the cross-product union is an
16802-
// intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions) and
16803-
// the denormalized origin has fewer constituents than the union itself.
16804-
const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) && getConstituentCountOfTypes(constituents) > getConstituentCountOfTypes(typeSet) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, typeSet) : undefined;
16805-
result = getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
16870+
if (runningResult && runningResult !== typeSet[0]) {
16871+
result = getIntersectionType([runningResult, getUnionType(constituents, UnionReduction.Literal)], aliasSymbol, aliasTypeArguments);
16872+
}
16873+
else {
16874+
// We attach a denormalized origin type when at least one constituent of the cross-product union is an
16875+
// intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions) and
16876+
// the denormalized origin has fewer constituents than the union itself.
16877+
const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) && getConstituentCountOfTypes(constituents) > getConstituentCountOfTypes(typeSet) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, originalSet) : undefined;
16878+
result = getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
16879+
}
1680616880
}
1680716881
}
1680816882
else {
@@ -16819,7 +16893,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1681916893

1682016894
function checkCrossProductUnion(types: readonly Type[]) {
1682116895
const size = getCrossProductUnionSize(types);
16822-
if (size >= 100000) {
16896+
if (size >= UNION_CROSS_PRODUCT_SIZE_LIMIT) {
1682316897
tracing?.instant(tracing.Phase.CheckTypes, "checkCrossProductUnion_DepthLimit", { typeIds: types.map(t => t.id), size });
1682416898
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
1682516899
return false;

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5859,6 +5859,7 @@ export interface SymbolLinks {
58595859
variances?: VarianceFlags[]; // Alias symbol type argument variance cache
58605860
deferralConstituents?: Type[]; // Calculated list of constituents for a deferred type
58615861
deferralWriteConstituents?: Type[]; // Constituents of a deferred `writeType`
5862+
deferredMismatchedParts?: Symbol[]; // If a `(A | (B & C))["ref"]` is deferred, the `B["ref"]` and `C["ref"]` symbols needed to un-defer the outer type
58625863
deferralParent?: Type; // Source union/intersection of a deferred type
58635864
cjsExportMerged?: Symbol; // Version of the symbol with all non export= exports merged with the export= target
58645865
typeOnlyDeclaration?: TypeOnlyAliasDeclaration | false; // First resolved alias declaration that makes the symbol only usable in type constructs

0 commit comments

Comments
 (0)