From b1316e589e101cc6a438dcbd9a70224d97211bde Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 30 Nov 2017 09:33:05 -0800 Subject: [PATCH 1/5] Cut off inference for recursive mapped types Previously, when inferring to a self-referential (or otherwise recursive) homomorphic mapped type from a source type that also has recursive references, type inference would enter infinite recursion. Now there is a more complex stack for mapped type inference. It mirrors the existing symbolStack but (1) includes the source type and (2) is passed through inferTypeForHomomorphicMappedType, which is actually called outside of inferTypes, and so restarts the symbolStack cache every time. --- src/compiler/checker.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cd4eeed7522..8fa124868cc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11127,7 +11127,7 @@ namespace ts { * property is computed by inferring from the source property type to X for the type * variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). */ - function inferTypeForHomomorphicMappedType(source: Type, target: MappedType): Type { + function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, mappedTypeStack: [Type, Symbol][]): Type { const properties = getPropertiesOfType(source); let indexInfo = getIndexInfoOfType(source, IndexKind.String); if (properties.length === 0 && !indexInfo) { @@ -11160,7 +11160,7 @@ namespace ts { function inferTargetType(sourceType: Type): Type { inference.candidates = undefined; - inferTypes(inferences, sourceType, templateType); + inferTypes(inferences, sourceType, templateType, 0, mappedTypeStack); return inference.candidates ? getUnionType(inference.candidates, /*subtypeReduction*/ true) : emptyObjectType; } } @@ -11178,7 +11178,7 @@ namespace ts { return undefined; } - function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0) { + function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, mappedTypeStack?: [Type, Symbol][]) { let symbolStack: Symbol[]; let visited: Map; inferFromTypes(originalSource, originalTarget); @@ -11395,7 +11395,12 @@ namespace ts { // such that direct inferences to T get priority over inferences to Partial, for example. const inference = getInferenceInfoForType((constraintType).type); if (inference && !inference.isFixed) { - const inferredType = inferTypeForHomomorphicMappedType(source, target); + if (contains(mappedTypeStack, [source, target.symbol], ([s1,t1],[s2,t2]) => s1 === s2 && t1 === t2)) { + return; + } + (mappedTypeStack || (mappedTypeStack = [])).push([source, target.symbol]); + const inferredType = inferTypeForHomomorphicMappedType(source, target, mappedTypeStack); + mappedTypeStack.pop(); if (inferredType) { const savePriority = priority; priority |= InferencePriority.MappedType; From 1068ee105d71d0656b365fd96a8b86751d1d2bed Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 30 Nov 2017 09:40:43 -0800 Subject: [PATCH 2/5] Test:inference to self-referential mapped type From a self-referential type. --- .../reference/mappedTypeRecursiveInference.js | 10 ++++++ .../mappedTypeRecursiveInference.symbols | 32 ++++++++++++++++++ .../mappedTypeRecursiveInference.types | 33 +++++++++++++++++++ .../compiler/mappedTypeRecursiveInference.ts | 5 +++ 4 files changed, 80 insertions(+) create mode 100644 tests/baselines/reference/mappedTypeRecursiveInference.js create mode 100644 tests/baselines/reference/mappedTypeRecursiveInference.symbols create mode 100644 tests/baselines/reference/mappedTypeRecursiveInference.types create mode 100644 tests/cases/compiler/mappedTypeRecursiveInference.ts diff --git a/tests/baselines/reference/mappedTypeRecursiveInference.js b/tests/baselines/reference/mappedTypeRecursiveInference.js new file mode 100644 index 00000000000..5b7631818bc --- /dev/null +++ b/tests/baselines/reference/mappedTypeRecursiveInference.js @@ -0,0 +1,10 @@ +//// [mappedTypeRecursiveInference.ts] +interface A { a: A } +declare let a: A; +type Deep = { [K in keyof T]: Deep } +declare function foo(deep: Deep): T; +const out = foo(a); + + +//// [mappedTypeRecursiveInference.js] +var out = foo(a); diff --git a/tests/baselines/reference/mappedTypeRecursiveInference.symbols b/tests/baselines/reference/mappedTypeRecursiveInference.symbols new file mode 100644 index 00000000000..a3fa3722551 --- /dev/null +++ b/tests/baselines/reference/mappedTypeRecursiveInference.symbols @@ -0,0 +1,32 @@ +=== tests/cases/compiler/mappedTypeRecursiveInference.ts === +interface A { a: A } +>A : Symbol(A, Decl(mappedTypeRecursiveInference.ts, 0, 0)) +>a : Symbol(A.a, Decl(mappedTypeRecursiveInference.ts, 0, 13)) +>A : Symbol(A, Decl(mappedTypeRecursiveInference.ts, 0, 0)) + +declare let a: A; +>a : Symbol(a, Decl(mappedTypeRecursiveInference.ts, 1, 11)) +>A : Symbol(A, Decl(mappedTypeRecursiveInference.ts, 0, 0)) + +type Deep = { [K in keyof T]: Deep } +>Deep : Symbol(Deep, Decl(mappedTypeRecursiveInference.ts, 1, 17)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 2, 10)) +>K : Symbol(K, Decl(mappedTypeRecursiveInference.ts, 2, 18)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 2, 10)) +>Deep : Symbol(Deep, Decl(mappedTypeRecursiveInference.ts, 1, 17)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 2, 10)) +>K : Symbol(K, Decl(mappedTypeRecursiveInference.ts, 2, 18)) + +declare function foo(deep: Deep): T; +>foo : Symbol(foo, Decl(mappedTypeRecursiveInference.ts, 2, 45)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 3, 21)) +>deep : Symbol(deep, Decl(mappedTypeRecursiveInference.ts, 3, 24)) +>Deep : Symbol(Deep, Decl(mappedTypeRecursiveInference.ts, 1, 17)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 3, 21)) +>T : Symbol(T, Decl(mappedTypeRecursiveInference.ts, 3, 21)) + +const out = foo(a); +>out : Symbol(out, Decl(mappedTypeRecursiveInference.ts, 4, 5)) +>foo : Symbol(foo, Decl(mappedTypeRecursiveInference.ts, 2, 45)) +>a : Symbol(a, Decl(mappedTypeRecursiveInference.ts, 1, 11)) + diff --git a/tests/baselines/reference/mappedTypeRecursiveInference.types b/tests/baselines/reference/mappedTypeRecursiveInference.types new file mode 100644 index 00000000000..382135591d5 --- /dev/null +++ b/tests/baselines/reference/mappedTypeRecursiveInference.types @@ -0,0 +1,33 @@ +=== tests/cases/compiler/mappedTypeRecursiveInference.ts === +interface A { a: A } +>A : A +>a : A +>A : A + +declare let a: A; +>a : A +>A : A + +type Deep = { [K in keyof T]: Deep } +>Deep : Deep +>T : T +>K : K +>T : T +>Deep : Deep +>T : T +>K : K + +declare function foo(deep: Deep): T; +>foo : (deep: Deep) => T +>T : T +>deep : Deep +>Deep : Deep +>T : T +>T : T + +const out = foo(a); +>out : { a: {}; } +>foo(a) : { a: {}; } +>foo : (deep: Deep) => T +>a : A + diff --git a/tests/cases/compiler/mappedTypeRecursiveInference.ts b/tests/cases/compiler/mappedTypeRecursiveInference.ts new file mode 100644 index 00000000000..d7a9cf3a0be --- /dev/null +++ b/tests/cases/compiler/mappedTypeRecursiveInference.ts @@ -0,0 +1,5 @@ +interface A { a: A } +declare let a: A; +type Deep = { [K in keyof T]: Deep } +declare function foo(deep: Deep): T; +const out = foo(a); From c7d53f8b701193203f469efbf57ed335a790b66d Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 30 Nov 2017 09:41:55 -0800 Subject: [PATCH 3/5] Fix whitespace lint --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8fa124868cc..329d40e188a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11395,7 +11395,7 @@ namespace ts { // such that direct inferences to T get priority over inferences to Partial, for example. const inference = getInferenceInfoForType((constraintType).type); if (inference && !inference.isFixed) { - if (contains(mappedTypeStack, [source, target.symbol], ([s1,t1],[s2,t2]) => s1 === s2 && t1 === t2)) { + if (contains(mappedTypeStack, [source, target.symbol], ([s1, t1], [s2, t2]) => s1 === s2 && t1 === t2)) { return; } (mappedTypeStack || (mappedTypeStack = [])).push([source, target.symbol]); From 0e5b53579ddff988dbc8061a1a24958e2d8b68c7 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 1 Dec 2017 14:43:20 -0800 Subject: [PATCH 4/5] mappedTypeStack uses a string based on symbols Previously it was a pair of [Type, Symbol]. --- src/compiler/checker.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 329d40e188a..f0cb233cb75 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11127,7 +11127,7 @@ namespace ts { * property is computed by inferring from the source property type to X for the type * variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). */ - function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, mappedTypeStack: [Type, Symbol][]): Type { + function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, mappedTypeStack: string[]): Type { const properties = getPropertiesOfType(source); let indexInfo = getIndexInfoOfType(source, IndexKind.String); if (properties.length === 0 && !indexInfo) { @@ -11178,7 +11178,7 @@ namespace ts { return undefined; } - function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, mappedTypeStack?: [Type, Symbol][]) { + function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, mappedTypeStack?: string[]) { let symbolStack: Symbol[]; let visited: Map; inferFromTypes(originalSource, originalTarget); @@ -11395,10 +11395,11 @@ namespace ts { // such that direct inferences to T get priority over inferences to Partial, for example. const inference = getInferenceInfoForType((constraintType).type); if (inference && !inference.isFixed) { - if (contains(mappedTypeStack, [source, target.symbol], ([s1, t1], [s2, t2]) => s1 === s2 && t1 === t2)) { + const key = (source.symbol ? getSymbolId(source.symbol) : "no symbol") + "," + getSymbolId(target.symbol); + if (contains(mappedTypeStack, key)) { return; } - (mappedTypeStack || (mappedTypeStack = [])).push([source, target.symbol]); + (mappedTypeStack || (mappedTypeStack = [])).push(key); const inferredType = inferTypeForHomomorphicMappedType(source, target, mappedTypeStack); mappedTypeStack.pop(); if (inferredType) { From 84d747a89cca17e70ff797754f34bc14807f8f03 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 1 Dec 2017 14:48:59 -0800 Subject: [PATCH 5/5] Simplify mappedTypeStack cache key --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f0cb233cb75..c77f9dd6527 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11395,7 +11395,7 @@ namespace ts { // such that direct inferences to T get priority over inferences to Partial, for example. const inference = getInferenceInfoForType((constraintType).type); if (inference && !inference.isFixed) { - const key = (source.symbol ? getSymbolId(source.symbol) : "no symbol") + "," + getSymbolId(target.symbol); + const key = (source.symbol ? getSymbolId(source.symbol) + "," : "") + getSymbolId(target.symbol); if (contains(mappedTypeStack, key)) { return; }