Skip to content

Always fully compute variance structurally #48080

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
78 changes: 52 additions & 26 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ namespace ts {
let instantiationCount = 0;
let instantiationDepth = 0;
let inlineLevel = 0;
let varianceLevel = 0;
let nestedVarianceSymbols: Symbol[] | undefined;
let incompleteVariancesObserved = false;
let currentNode: Node | undefined;

const emptySymbols = createSymbolTable();
Expand Down Expand Up @@ -17947,7 +17950,7 @@ namespace ts {
}
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) {
const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false));
if (related !== undefined) {
if (related !== undefined && !(related & RelationComparisonResult.ReportsMask)) {
return !!(related & RelationComparisonResult.Succeeded);
}
}
Expand Down Expand Up @@ -20366,35 +20369,49 @@ namespace ts {
return false;
}

function getVariances(type: GenericType): VarianceFlags[] {
// Arrays and tuples are known to be covariant, no need to spend time computing this.
if (type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ObjectFlags.Tuple) {
return arrayVariances;
}
return getVariancesWorker(type.symbol, type.typeParameters, getMarkerTypeReference);
}

// Return a type reference where the source type parameter is replaced with the target marker
// type, and flag the result as a marker type reference.
function getMarkerTypeReference(type: GenericType, source: TypeParameter, target: Type) {
function getMarkerTypeReference(symbol: Symbol, source: TypeParameter, target: Type) {
const type = getDeclaredTypeOfSymbol(symbol) as GenericType;
const result = createTypeReference(type, map(type.typeParameters, t => t === source ? target : t));
result.objectFlags |= ObjectFlags.MarkerType;
return result;
}

function getAliasVariances(symbol: Symbol) {
const links = getSymbolLinks(symbol);
return getVariancesWorker(links.typeParameters, links, (_links, param, marker) => {
const type = getTypeAliasInstantiation(symbol, instantiateTypes(links.typeParameters!, makeUnaryTypeMapper(param, marker)));
type.aliasTypeArgumentsContainsMarker = true;
return type;
});
return getVariancesWorker(symbol, getSymbolLinks(symbol).typeParameters, getMarkerTypeAliasReference);
}

function getMarkerTypeAliasReference(symbol: Symbol, source: TypeParameter, target: Type) {
const result = getTypeAliasInstantiation(symbol, instantiateTypes(getSymbolLinks(symbol).typeParameters!, makeUnaryTypeMapper(source, target)));
result.aliasTypeArgumentsContainsMarker = true;
return result;
}

// Return an array containing the variance of each type parameter. The variance is effectively
// a digest of the type comparisons that occur for each type argument when instantiations of the
// generic type are structurally compared. We infer the variance information by comparing
// instantiations of the generic type for type arguments with known relations. The function
// returns the emptyArray singleton when invoked recursively for the given generic type.
function getVariancesWorker<TCache extends { variances?: VarianceFlags[] }>(typeParameters: readonly TypeParameter[] = emptyArray, cache: TCache, createMarkerType: (input: TCache, param: TypeParameter, marker: Type) => Type): VarianceFlags[] {
let variances = cache.variances;
if (!variances) {
tracing?.push(tracing.Phase.CheckTypes, "getVariancesWorker", { arity: typeParameters.length, id: (cache as any).id ?? (cache as any).declaredType?.id ?? -1 });
function getVariancesWorker(symbol: Symbol, typeParameters: readonly TypeParameter[] = emptyArray, createMarkerType: (symbol: Symbol, param: TypeParameter, marker: Type) => Type): VarianceFlags[] {
const links = getSymbolLinks(symbol);
if (!links.variances) {
tracing?.push(tracing.Phase.CheckTypes, "getVariancesWorker", { arity: typeParameters.length, id: getSymbolId(symbol) });
if (varianceLevel > 0) {
nestedVarianceSymbols = append(nestedVarianceSymbols, symbol);
}
varianceLevel++;
// The emptyArray singleton is used to signal a recursive invocation.
cache.variances = emptyArray;
variances = [];
links.variances = emptyArray;
const variances = [];
for (const tp of typeParameters) {
let unmeasurable = false;
let unreliable = false;
Expand All @@ -20403,15 +20420,15 @@ namespace ts {
// We first compare instantiations where the type parameter is replaced with
// marker types that have a known subtype relationship. From this we can infer
// invariance, covariance, contravariance or bivariance.
const typeWithSuper = createMarkerType(cache, tp, markerSuperType);
const typeWithSub = createMarkerType(cache, tp, markerSubType);
const typeWithSuper = createMarkerType(symbol, tp, markerSuperType);
const typeWithSub = createMarkerType(symbol, tp, markerSubType);
let variance = (isTypeAssignableTo(typeWithSub, typeWithSuper) ? VarianceFlags.Covariant : 0) |
(isTypeAssignableTo(typeWithSuper, typeWithSub) ? VarianceFlags.Contravariant : 0);
// If the instantiations appear to be related bivariantly it may be because the
// type parameter is independent (i.e. it isn't witnessed anywhere in the generic
// type). To determine this we compare instantiations where the type parameter is
// replaced with marker types that are known to be unrelated.
if (variance === VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(cache, tp, markerOtherType), typeWithSuper)) {
if (variance === VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(symbol, tp, markerOtherType), typeWithSuper)) {
variance = VarianceFlags.Independent;
}
outofbandVarianceMarkerHandler = oldHandler;
Expand All @@ -20425,18 +20442,27 @@ namespace ts {
}
variances.push(variance);
}
cache.variances = variances;
links.variances = variances;
varianceLevel--;
// Recursive invocations of getVariancesWorker occur when two or more types circularly reference each
// other. In such cases, the nested invocations might observe in-process variance computations, i.e.
// cases where getVariancesWorker returns emptyArray. If that happens we clear (and thus re-compute) the
// results of nested variance computations and only permanently record the outermost result. See #44572.
if (varianceLevel === 0) {
if (nestedVarianceSymbols && incompleteVariancesObserved) {
for (const sym of nestedVarianceSymbols) {
getSymbolLinks(sym).variances = undefined;
}
}
nestedVarianceSymbols = undefined;
incompleteVariancesObserved = false;
}
tracing?.pop();
}
return variances;
}

function getVariances(type: GenericType): VarianceFlags[] {
// Arrays and tuples are known to be covariant, no need to spend time computing this.
if (type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ObjectFlags.Tuple) {
return arrayVariances;
else {
incompleteVariancesObserved ||= links.variances === emptyArray;
}
return getVariancesWorker(type.typeParameters, type, getMarkerTypeReference);
return links.variances;
}

// Return true if the given type reference has a 'void' type argument for a covariant type parameter.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
tests/cases/compiler/checkOrderDependenceGenericAssignability.ts(21,1): error TS2322: Type 'Parent1<unknown>' is not assignable to type 'Parent1<string>'.
Type 'unknown' is not assignable to type 'string'.
tests/cases/compiler/checkOrderDependenceGenericAssignability.ts(45,1): error TS2322: Type 'Parent2<unknown>' is not assignable to type 'Parent2<string>'.
Type 'unknown' is not assignable to type 'string'.
tests/cases/compiler/checkOrderDependenceGenericAssignability.ts(62,1): error TS2322: Type 'Child3<unknown>' is not assignable to type 'Child3<string>'.
Type 'unknown' is not assignable to type 'string'.
tests/cases/compiler/checkOrderDependenceGenericAssignability.ts(68,1): error TS2322: Type 'Parent3<unknown>' is not assignable to type 'Parent3<string>'.
Type 'unknown' is not assignable to type 'string'.


==== tests/cases/compiler/checkOrderDependenceGenericAssignability.ts (4 errors) ====
// Repro from #44572 with interface types

interface Parent1<A> {
child: Child1<A>;
parent: Parent1<A>;
}

interface Child1<A, B = unknown> extends Parent1<A> {
readonly a: A;
readonly b: B;
}

function fn1<A>(inp: Child1<A>) {
const a: Child1<unknown> = inp;
}

declare let pu1: Parent1<unknown>;
declare let ps1: Parent1<string>;

pu1 = ps1; // Ok
ps1 = pu1; // Error expected
~~~
!!! error TS2322: Type 'Parent1<unknown>' is not assignable to type 'Parent1<string>'.
!!! error TS2322: Type 'unknown' is not assignable to type 'string'.

// Repro from #44572 with aliased object types

type Parent2<A> = {
child: Child2<A>;
parent: Parent2<A>;
}

type Child2<A, B = unknown> = {
child: Child2<A>;
parent: Parent2<A>;
readonly a: A;
readonly b: B;
}

function fn2<A>(inp: Child2<A>) {
const a: Child2<unknown> = inp;
}

declare let pu2: Parent2<unknown>;
declare let ps2: Parent2<string>;

pu2 = ps2; // Ok
ps2 = pu2; // Error expected
~~~
!!! error TS2322: Type 'Parent2<unknown>' is not assignable to type 'Parent2<string>'.
!!! error TS2322: Type 'unknown' is not assignable to type 'string'.

// Simpler repro for same issue

interface Parent3<A> {
child: Child3<A[]>;
parent: Parent3<A>;
}

interface Child3<A> extends Parent3<A> {
readonly a: A;
}

declare let cu3: Child3<unknown>;
declare let cs3: Child3<string>;

cu3 = cs3; // Ok
cs3 = cu3; // Error expected
~~~
!!! error TS2322: Type 'Child3<unknown>' is not assignable to type 'Child3<string>'.
!!! error TS2322: Type 'unknown' is not assignable to type 'string'.

declare let pu3: Parent3<unknown>;
declare let ps3: Parent3<string>;

pu3 = ps3; // Ok
ps3 = pu3; // Error expected
~~~
!!! error TS2322: Type 'Parent3<unknown>' is not assignable to type 'Parent3<string>'.
!!! error TS2322: Type 'unknown' is not assignable to type 'string'.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//// [checkOrderDependenceGenericAssignability.ts]
// Repro from #44572 with interface types

interface Parent1<A> {
child: Child1<A>;
parent: Parent1<A>;
}

interface Child1<A, B = unknown> extends Parent1<A> {
readonly a: A;
readonly b: B;
}

function fn1<A>(inp: Child1<A>) {
const a: Child1<unknown> = inp;
}

declare let pu1: Parent1<unknown>;
declare let ps1: Parent1<string>;

pu1 = ps1; // Ok
ps1 = pu1; // Error expected

// Repro from #44572 with aliased object types

type Parent2<A> = {
child: Child2<A>;
parent: Parent2<A>;
}

type Child2<A, B = unknown> = {
child: Child2<A>;
parent: Parent2<A>;
readonly a: A;
readonly b: B;
}

function fn2<A>(inp: Child2<A>) {
const a: Child2<unknown> = inp;
}

declare let pu2: Parent2<unknown>;
declare let ps2: Parent2<string>;

pu2 = ps2; // Ok
ps2 = pu2; // Error expected

// Simpler repro for same issue

interface Parent3<A> {
child: Child3<A[]>;
parent: Parent3<A>;
}

interface Child3<A> extends Parent3<A> {
readonly a: A;
}

declare let cu3: Child3<unknown>;
declare let cs3: Child3<string>;

cu3 = cs3; // Ok
cs3 = cu3; // Error expected

declare let pu3: Parent3<unknown>;
declare let ps3: Parent3<string>;

pu3 = ps3; // Ok
ps3 = pu3; // Error expected


//// [checkOrderDependenceGenericAssignability.js]
"use strict";
// Repro from #44572 with interface types
function fn1(inp) {
var a = inp;
}
pu1 = ps1; // Ok
ps1 = pu1; // Error expected
function fn2(inp) {
var a = inp;
}
pu2 = ps2; // Ok
ps2 = pu2; // Error expected
cu3 = cs3; // Ok
cs3 = cu3; // Error expected
pu3 = ps3; // Ok
ps3 = pu3; // Error expected
Loading