From 1ea52a45e261e99763d4d372eee765bf9cb4c68b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 13 Feb 2024 15:36:29 -0800 Subject: [PATCH 1/4] Propegate outer type parameters of single signature types --- src/compiler/checker.ts | 56 ++++++++++++++----- src/compiler/types.ts | 7 +++ .../reference/nestedGenericSpreadInference.js | 14 +++++ .../nestedGenericSpreadInference.symbols | 34 +++++++++++ .../nestedGenericSpreadInference.types | 27 +++++++++ .../compiler/nestedGenericSpreadInference.ts | 6 ++ 6 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 tests/baselines/reference/nestedGenericSpreadInference.js create mode 100644 tests/baselines/reference/nestedGenericSpreadInference.symbols create mode 100644 tests/baselines/reference/nestedGenericSpreadInference.types create mode 100644 tests/cases/compiler/nestedGenericSpreadInference.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0d12a7f74fdb4..78d3aab1ad132 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -965,6 +965,7 @@ import { SignatureFlags, SignatureKind, singleElementArray, + SingleSignatureType, skipOuterExpressions, skipParentheses, skipTrivia, @@ -7088,7 +7089,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const abstractSignatures = filter(resolved.constructSignatures, signature => !!(signature.flags & SignatureFlags.Abstract)); if (some(abstractSignatures)) { - const types = map(abstractSignatures, getOrCreateTypeFromSignature); + const types = map(abstractSignatures, s => getOrCreateTypeFromSignature(s)); // count the number of type elements excluding abstract constructors const typeElementCount = resolved.callSignatures.length + (resolved.constructSignatures.length - abstractSignatures.length) + @@ -15648,7 +15649,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return signature; } - function getOrCreateTypeFromSignature(signature: Signature): ObjectType { + function getOrCreateTypeFromSignature(signature: Signature, outerTypeParameters?: TypeParameter[]): ObjectType { // There are two ways to declare a construct signature, one is by declaring a class constructor // using the constructor keyword, and the other is declaring a bare construct signature in an // object type literal or interface (using the new keyword). Each way of declaring a constructor @@ -15659,7 +15660,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If declaration is undefined, it is likely to be the signature of the default constructor. const isConstructor = kind === undefined || kind === SyntaxKind.Constructor || kind === SyntaxKind.ConstructSignature || kind === SyntaxKind.ConstructorType; - const type = createObjectType(ObjectFlags.Anonymous); + // The type must have a symbol with a `Function` flag and a declaration in order to be correctly flagged as possibly containing + // type variables by `couldContainTypeVariables` + const type = createObjectType(ObjectFlags.Anonymous | ObjectFlags.SingleSignatureType, createSymbol(SymbolFlags.Function, InternalSymbolName.Function)) as SingleSignatureType; + if (signature.declaration) { + type.symbol.declarations = [signature.declaration]; + type.symbol.valueDeclaration = signature.declaration; + } + outerTypeParameters ||= signature.declaration && getOuterTypeParameters(signature.declaration, /*includeThisTypes*/ true); + type.outerTypeParameters = outerTypeParameters; + type.members = emptySymbols; type.properties = emptyArray; type.callSignatures = !isConstructor ? [signature] : emptyArray; @@ -19630,7 +19640,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const links = getNodeLinks(declaration); const target = type.objectFlags & ObjectFlags.Reference ? links.resolvedType! as DeferredTypeReference : type.objectFlags & ObjectFlags.Instantiated ? type.target! : type; - let typeParameters = links.outerTypeParameters; + let typeParameters = type.objectFlags & ObjectFlags.SingleSignatureType ? (type as SingleSignatureType).outerTypeParameters : links.outerTypeParameters; if (!typeParameters) { // The first time an anonymous type is instantiated we compute and store a list of the type // parameters that are in scope (and therefore potentially referenced). For type literals that @@ -19860,6 +19870,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (type.objectFlags & ObjectFlags.InstantiationExpressionType) { (result as InstantiationExpressionType).node = (type as InstantiationExpressionType).node; } + if (type.objectFlags & ObjectFlags.SingleSignatureType) { + (result as SingleSignatureType).outerTypeParameters = (type as SingleSignatureType).outerTypeParameters; + } result.target = type; result.mapper = mapper; result.aliasSymbol = aliasSymbol || type.aliasSymbol; @@ -25483,6 +25496,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } + /** + * @returns `true` if `type` has the shape `[T[0]]` where `T` is `typeParameter` + */ + function isTupleOfSelf(typeParameter: TypeParameter, type: Type) { + return isTupleType(type) && getTupleElementType(type, 0) === getIndexedAccessType(typeParameter, getNumberLiteralType(0)) && !getTypeOfPropertyOfType(type, "1" as __String); + } + function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority = InferencePriority.None, contravariant = false) { let bivariant = false; let propagationType: Type; @@ -25611,6 +25631,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { inference.priority = priority; } if (priority === inference.priority) { + // Inferring A to [A[0]] is a zero information inference (it guarantees A becomes its constraint), but oft arises from generic argument list inferences + // By discarding it early, we can allow more fruitful results to be used instead. + if (isTupleOfSelf(inference.typeParameter, candidate)) { + return; + } // We make contravariant inferences only if we are in a pure contravariant position, // i.e. only if we have not descended into a bivariant position. if (contravariant && !bivariant) { @@ -26347,23 +26372,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // inference error only occurs when there are *conflicting* candidates, i.e. // candidates with no common supertype. const defaultType = getDefaultFromTypeParameter(inference.typeParameter); - if (defaultType) { - // Instantiate the default type. Any forward reference to a type - // parameter should be instantiated to the empty object type. - inferredType = instantiateType(defaultType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); - } + inferredType = defaultType; } } else { inferredType = getTypeFromInference(inference); } - inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); + const isDefault = !inferredType; + inferredType ||= getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); + inference.inferredType = inferredType; + // Instantiate the inferred type. Any forward reference to a type + // parameter should be instantiated to the empty object type. + inferredType = instantiateType(inferredType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); + inference.inferredType = inferredType; const constraint = getConstraintOfTypeParameter(inference.typeParameter); if (constraint) { const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); - if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { + if (isDefault || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { // If the fallback type satisfies the constraint, we pick it. Otherwise, we pick the constraint. inference.inferredType = fallbackType && context.compareTypes(fallbackType, getTypeWithThisArgument(instantiatedConstraint, fallbackType)) ? fallbackType : instantiatedConstraint; } @@ -34265,7 +34292,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive), // we obtain the regular type of any object literal arguments because we may not have inferred complete // parameter types yet and therefore excess property checks may yield false positives (see #17041). - const checkArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType; + const checkArgType = instantiateType(checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType, signature.mapper); const effectiveCheckArgumentNode = getEffectiveCheckNode(arg); if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? effectiveCheckArgumentNode : undefined, effectiveCheckArgumentNode, headMessage, containingMessageChain, errorOutputContainer)) { Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); @@ -39287,7 +39314,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } } - return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context)); + // TODO: The signature may reference any outer inference contexts, but we map pop off and then apply new inference contexts, and thus get different inferred types. + // That this is cached on the *first* such attempt is not currently an issue, since expression types *also* get cached on the first pass. If we ever properly speculate, though, + // the cached "isolatedSignatureType" signature field absolutely needs to be included in the list of speculative caches. + return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context), flatMap(inferenceContexts, c => c && map(c.inferences, i => i.typeParameter)).slice()); } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 28061c88e94bb..a56980783e4b8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6317,6 +6317,7 @@ export const enum ObjectFlags { ContainsSpread = 1 << 21, // Object literal contains spread operation ObjectRestType = 1 << 22, // Originates in object rest declaration InstantiationExpressionType = 1 << 23, // Originates in instantiation expression + SingleSignatureType = 1 << 27, // A single signature type extracted from a potentially broader type /** @internal */ IsClassInstanceClone = 1 << 24, // Type is a clone of a class instance type // Flags that require TypeFlags.Object and ObjectFlags.Reference @@ -6527,6 +6528,12 @@ export interface AnonymousType extends ObjectType { instantiations?: Map; // Instantiations of generic type alias (undefined if non-generic) } +/** @internal */ +// A SingleSignatureType may have bespoke outer type parameters to handle free type variable inferences +export interface SingleSignatureType extends AnonymousType { + outerTypeParameters?: TypeParameter[]; +} + /** @internal */ export interface InstantiationExpressionType extends AnonymousType { node: NodeWithTypeArguments; diff --git a/tests/baselines/reference/nestedGenericSpreadInference.js b/tests/baselines/reference/nestedGenericSpreadInference.js new file mode 100644 index 0000000000000..00bb4a558c2d4 --- /dev/null +++ b/tests/baselines/reference/nestedGenericSpreadInference.js @@ -0,0 +1,14 @@ +//// [tests/cases/compiler/nestedGenericSpreadInference.ts] //// + +//// [nestedGenericSpreadInference.ts] +declare function wrap(x: X): { x: X }; +declare function call(x: { x: (...args: A) => T }, ...args: A): T; + +// This should be of type `number` - ideally, it also would not error. +const leak = call(wrap((x: T) => x), 1); + + +//// [nestedGenericSpreadInference.js] +"use strict"; +// This should be of type `number` - ideally, it also would not error. +var leak = call(wrap(function (x) { return x; }), 1); diff --git a/tests/baselines/reference/nestedGenericSpreadInference.symbols b/tests/baselines/reference/nestedGenericSpreadInference.symbols new file mode 100644 index 0000000000000..bef06547b70ec --- /dev/null +++ b/tests/baselines/reference/nestedGenericSpreadInference.symbols @@ -0,0 +1,34 @@ +//// [tests/cases/compiler/nestedGenericSpreadInference.ts] //// + +=== nestedGenericSpreadInference.ts === +declare function wrap(x: X): { x: X }; +>wrap : Symbol(wrap, Decl(nestedGenericSpreadInference.ts, 0, 0)) +>X : Symbol(X, Decl(nestedGenericSpreadInference.ts, 0, 22)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 0, 25)) +>X : Symbol(X, Decl(nestedGenericSpreadInference.ts, 0, 22)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 0, 33)) +>X : Symbol(X, Decl(nestedGenericSpreadInference.ts, 0, 22)) + +declare function call(x: { x: (...args: A) => T }, ...args: A): T; +>call : Symbol(call, Decl(nestedGenericSpreadInference.ts, 0, 41)) +>A : Symbol(A, Decl(nestedGenericSpreadInference.ts, 1, 22)) +>T : Symbol(T, Decl(nestedGenericSpreadInference.ts, 1, 42)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 1, 46)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 1, 50)) +>args : Symbol(args, Decl(nestedGenericSpreadInference.ts, 1, 55)) +>A : Symbol(A, Decl(nestedGenericSpreadInference.ts, 1, 22)) +>T : Symbol(T, Decl(nestedGenericSpreadInference.ts, 1, 42)) +>args : Symbol(args, Decl(nestedGenericSpreadInference.ts, 1, 74)) +>A : Symbol(A, Decl(nestedGenericSpreadInference.ts, 1, 22)) +>T : Symbol(T, Decl(nestedGenericSpreadInference.ts, 1, 42)) + +// This should be of type `number` - ideally, it also would not error. +const leak = call(wrap((x: T) => x), 1); +>leak : Symbol(leak, Decl(nestedGenericSpreadInference.ts, 4, 5)) +>call : Symbol(call, Decl(nestedGenericSpreadInference.ts, 0, 41)) +>wrap : Symbol(wrap, Decl(nestedGenericSpreadInference.ts, 0, 0)) +>T : Symbol(T, Decl(nestedGenericSpreadInference.ts, 4, 24)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 4, 27)) +>T : Symbol(T, Decl(nestedGenericSpreadInference.ts, 4, 24)) +>x : Symbol(x, Decl(nestedGenericSpreadInference.ts, 4, 27)) + diff --git a/tests/baselines/reference/nestedGenericSpreadInference.types b/tests/baselines/reference/nestedGenericSpreadInference.types new file mode 100644 index 0000000000000..7d62f13b35347 --- /dev/null +++ b/tests/baselines/reference/nestedGenericSpreadInference.types @@ -0,0 +1,27 @@ +//// [tests/cases/compiler/nestedGenericSpreadInference.ts] //// + +=== nestedGenericSpreadInference.ts === +declare function wrap(x: X): { x: X }; +>wrap : (x: X) => { x: X;} +>x : X +>x : X + +declare function call(x: { x: (...args: A) => T }, ...args: A): T; +>call : (x: { x: (...args: A) => T; }, ...args: A) => T +>x : { x: (...args: A) => T; } +>x : (...args: A) => T +>args : A +>args : A + +// This should be of type `number` - ideally, it also would not error. +const leak = call(wrap((x: T) => x), 1); +>leak : number +>call(wrap((x: T) => x), 1) : number +>call : (x: { x: (...args: A) => T; }, ...args: A) => T +>wrap((x: T) => x) : { x: (x: A[0]) => A[0]; } +>wrap : (x: X) => { x: X; } +>(x: T) => x : (x: T) => T +>x : T +>x : T +>1 : 1 + diff --git a/tests/cases/compiler/nestedGenericSpreadInference.ts b/tests/cases/compiler/nestedGenericSpreadInference.ts new file mode 100644 index 0000000000000..14e606f5f5cbf --- /dev/null +++ b/tests/cases/compiler/nestedGenericSpreadInference.ts @@ -0,0 +1,6 @@ +// @strict: true +declare function wrap(x: X): { x: X }; +declare function call(x: { x: (...args: A) => T }, ...args: A): T; + +// This should be of type `number` - ideally, it also would not error. +const leak = call(wrap((x: T) => x), 1); From f2f9df02ee1b8ba13169bebc8b9a287f0c24651b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 20 Feb 2024 13:11:49 -0800 Subject: [PATCH 2/4] Move where instantiation happens out a call, clean up edge cases around signature self-references --- src/compiler/checker.ts | 46 ++++++++++++------- src/compiler/types.ts | 2 + tests/baselines/reference/api/typescript.d.ts | 1 + 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 78d3aab1ad132..35f961b16e4a5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15564,7 +15564,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } - function getSignatureInstantiation(signature: Signature, typeArguments: Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature { + function getSignatureInstantiation(signature: Signature, typeArguments: readonly Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature { const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, fillMissingTypeArguments(typeArguments, signature.typeParameters, getMinTypeArgumentCount(signature.typeParameters), isJavascript)); if (inferredTypeParameters) { const returnSignature = getSingleCallOrConstructSignature(getReturnTypeOfSignature(instantiatedSignature)); @@ -15628,6 +15628,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ); } + function getImplementationSignature(signature: Signature) { + return signature.typeParameters ? + signature.implementationSignatureCache ||= createImplementationSignature(signature) : + signature; + } + + function createImplementationSignature(signature: Signature) { + return signature.typeParameters ? instantiateSignature(signature, createTypeMapper([], [])) : signature; + } + function getBaseSignature(signature: Signature) { const typeParameters = signature.typeParameters; if (typeParameters) { @@ -15663,7 +15673,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // The type must have a symbol with a `Function` flag and a declaration in order to be correctly flagged as possibly containing // type variables by `couldContainTypeVariables` const type = createObjectType(ObjectFlags.Anonymous | ObjectFlags.SingleSignatureType, createSymbol(SymbolFlags.Function, InternalSymbolName.Function)) as SingleSignatureType; - if (signature.declaration) { + if (signature.declaration && !nodeIsSynthesized(signature.declaration)) { // skip synthetic declarations - keeping those around could be bad, since they lack a parent pointer type.symbol.declarations = [signature.declaration]; type.symbol.valueDeclaration = signature.declaration; } @@ -25139,6 +25149,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const result = !!(type.flags & TypeFlags.Instantiable || type.flags & TypeFlags.Object && !isNonGenericTopLevelType(type) && ( objectFlags & ObjectFlags.Reference && ((type as TypeReference).node || some(getTypeArguments(type as TypeReference), couldContainTypeVariables)) || + objectFlags & ObjectFlags.SingleSignatureType && !!length((type as SingleSignatureType).outerTypeParameters) || objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations || objectFlags & (ObjectFlags.Mapped | ObjectFlags.ReverseMapped | ObjectFlags.ObjectRestType | ObjectFlags.InstantiationExpressionType) ) || @@ -26372,25 +26383,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // inference error only occurs when there are *conflicting* candidates, i.e. // candidates with no common supertype. const defaultType = getDefaultFromTypeParameter(inference.typeParameter); - inferredType = defaultType; + // Instantiate the default type. Any forward reference to a type + // parameter should be instantiated to the empty object type. + inferredType = instantiateType(defaultType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); } } else { inferredType = getTypeFromInference(inference); } - const isDefault = !inferredType; - inferredType ||= getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); - inference.inferredType = inferredType; - // Instantiate the inferred type. Any forward reference to a type - // parameter should be instantiated to the empty object type. - inferredType = instantiateType(inferredType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); - inference.inferredType = inferredType; + inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); const constraint = getConstraintOfTypeParameter(inference.typeParameter); if (constraint) { const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); - if (isDefault || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { + if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { // If the fallback type satisfies the constraint, we pick it. Otherwise, we pick the constraint. inference.inferredType = fallbackType && context.compareTypes(fallbackType, getTypeWithThisArgument(instantiatedConstraint, fallbackType)) ? fallbackType : instantiatedConstraint; } @@ -34875,7 +34882,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { - const candidate = candidates[candidateIndex]; + let candidate = candidates[candidateIndex]; if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { continue; } @@ -34884,7 +34891,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let inferenceContext: InferenceContext | undefined; if (candidate.typeParameters) { - let typeArgumentTypes: Type[] | undefined; + // If we are *inside the body of candidate*, we need to create a clone of `candidate` with differing type parameter identities, + // so our inference results for this call doesn't pollute expression types referencing the outer type parameter! + if (candidate.declaration && findAncestor(node, a => a === candidate.declaration)) { + candidate = getImplementationSignature(candidate); + } + let typeArgumentTypes: readonly Type[] | undefined; if (some(typeArguments)) { typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); if (!typeArgumentTypes) { @@ -34893,8 +34905,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } else { - inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); - typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext); + inferenceContext = createInferenceContext(candidate.typeParameters!, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); + // The resulting type arguments are instantiated with the inference context mapper, as the inferred types may still contain references to the inference context's + // type variables via contextual projection. These are kept generic until all inferences are locked in, so the dependencies expressed can pass constraint checks. + typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext), inferenceContext.nonFixingMapper); argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal; } checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters); @@ -34919,7 +34933,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // round of type inference and applicability checking for this particular candidate. argCheckMode = CheckMode.Normal; if (inferenceContext) { - const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext); + const typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext), inferenceContext.mapper); checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext.inferredTypeParameters); // If the original signature has a generic rest type, instantiation may produce a // signature with different arity and we need to perform another arity check. diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a56980783e4b8..2c09a3e455b33 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6819,6 +6819,8 @@ export interface Signature { isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison /** @internal */ instantiations?: Map; // Generic signature instantiation cache + /** @internal */ + implementationSignatureCache?: Signature; // Copy of the signature with fresh type parameters to use in checking the body of a potentially self-referential generic function (deferred) } export const enum IndexKind { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 63d75271b18d4..dd4a9376a2be2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -7241,6 +7241,7 @@ declare namespace ts { ContainsSpread = 2097152, ObjectRestType = 4194304, InstantiationExpressionType = 8388608, + SingleSignatureType = 134217728, } interface ObjectType extends Type { objectFlags: ObjectFlags; From 0822f3395711bee18938bfde18293a428a59fa11 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 21 Feb 2024 14:28:25 -0800 Subject: [PATCH 3/4] Rather than creating new type identities, only instantiate argument expressions with the context, rather than the signature mapper --- src/compiler/checker.ts | 35 ++---- src/compiler/types.ts | 2 - ...lWithinOwnBodyCastTypeParameterIdentity.js | 52 +++++++++ ...inOwnBodyCastTypeParameterIdentity.symbols | 104 ++++++++++++++++++ ...thinOwnBodyCastTypeParameterIdentity.types | 83 ++++++++++++++ ...lWithinOwnBodyCastTypeParameterIdentity.ts | 26 +++++ 6 files changed, 277 insertions(+), 25 deletions(-) create mode 100644 tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js create mode 100644 tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols create mode 100644 tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types create mode 100644 tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f1157d3e57964..d307cb7787eaf 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15632,16 +15632,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ); } - function getImplementationSignature(signature: Signature) { - return signature.typeParameters ? - signature.implementationSignatureCache ||= createImplementationSignature(signature) : - signature; - } - - function createImplementationSignature(signature: Signature) { - return signature.typeParameters ? instantiateSignature(signature, createTypeMapper([], [])) : signature; - } - function getBaseSignature(signature: Signature) { const typeParameters = signature.typeParameters; if (typeParameters) { @@ -34256,6 +34246,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, + inferenceContext: InferenceContext | undefined, ): readonly Diagnostic[] | undefined { const errorOutputContainer: { errors?: Diagnostic[]; skipLogging?: boolean; } = { errors: undefined, skipLogging: true }; if (isJsxOpeningLikeElement(node)) { @@ -34290,7 +34281,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive), // we obtain the regular type of any object literal arguments because we may not have inferred complete // parameter types yet and therefore excess property checks may yield false positives (see #17041). - const checkArgType = instantiateType(checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType, signature.mapper); + const regularArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType; + // If this was inferred under a given inference context, we may need to instantiate the expression type to finish resolving + // the type variables in the expression. + const checkArgType = inferenceContext ? instantiateType(regularArgType, inferenceContext.nonFixingMapper) : regularArgType; const effectiveCheckArgumentNode = getEffectiveCheckNode(arg); if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? effectiveCheckArgumentNode : undefined, effectiveCheckArgumentNode, headMessage, containingMessageChain, errorOutputContainer)) { Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); @@ -34754,7 +34748,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (headMessage) { chain = chainDiagnosticMessages(chain, headMessage); } - const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain); + const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain, /*inferenceContext*/ undefined); if (diags) { for (const d of diags) { if (last.declaration && candidatesForArgumentError.length > 3) { @@ -34776,7 +34770,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let i = 0; for (const c of candidatesForArgumentError) { const chain = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Overload_0_of_1_2_gave_the_following_error, i + 1, candidates.length, signatureToString(c)); - const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain); + const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain, /*inferenceContext*/ undefined); if (diags) { if (diags.length <= min) { min = diags.length; @@ -34865,7 +34859,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (some(typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { return undefined; } - if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined, /*inferenceContext*/ undefined)) { candidatesForArgumentError = [candidate]; return undefined; } @@ -34873,7 +34867,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { - let candidate = candidates[candidateIndex]; + const candidate = candidates[candidateIndex]; if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { continue; } @@ -34882,11 +34876,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let inferenceContext: InferenceContext | undefined; if (candidate.typeParameters) { - // If we are *inside the body of candidate*, we need to create a clone of `candidate` with differing type parameter identities, - // so our inference results for this call doesn't pollute expression types referencing the outer type parameter! - if (candidate.declaration && findAncestor(node, a => a === candidate.declaration)) { - candidate = getImplementationSignature(candidate); - } let typeArgumentTypes: readonly Type[] | undefined; if (some(typeArguments)) { typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); @@ -34896,7 +34885,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } else { - inferenceContext = createInferenceContext(candidate.typeParameters!, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); + inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); // The resulting type arguments are instantiated with the inference context mapper, as the inferred types may still contain references to the inference context's // type variables via contextual projection. These are kept generic until all inferences are locked in, so the dependencies expressed can pass constraint checks. typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext), inferenceContext.nonFixingMapper); @@ -34913,7 +34902,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else { checkCandidate = candidate; } - if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined, inferenceContext)) { // Give preference to error candidates that have no rest parameters (as they are more specific) (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); continue; @@ -34933,7 +34922,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { continue; } } - if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined, inferenceContext)) { // Give preference to error candidates that have no rest parameters (as they are more specific) (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); continue; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2c09a3e455b33..a56980783e4b8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6819,8 +6819,6 @@ export interface Signature { isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison /** @internal */ instantiations?: Map; // Generic signature instantiation cache - /** @internal */ - implementationSignatureCache?: Signature; // Copy of the signature with fresh type parameters to use in checking the body of a potentially self-referential generic function (deferred) } export const enum IndexKind { diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js new file mode 100644 index 0000000000000..b28f5d95fd6b1 --- /dev/null +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js @@ -0,0 +1,52 @@ +//// [tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts] //// + +//// [genericCallWithinOwnBodyCastTypeParameterIdentity.ts] +interface Thenable { + then( + onFulfilled: (value: Value) => V | Thenable, + ): Thenable; +} + +const toThenable = (fn: (input: Input) => Result | Thenable) => + (input: Input): Thenable => { + const result = fn(input) + return { + then(onFulfilled: (value: Result) => V | Thenable) { + return toThenable(onFulfilled)(result as Result) + } + }; + } + +const toThenableInferred = (fn: (input: Input) => Result | Thenable) => + (input: Input): Thenable => { + const result = fn(input) + return { + then(onFulfilled) { + return toThenable(onFulfilled)(result as Result) + } + }; + } + + +//// [genericCallWithinOwnBodyCastTypeParameterIdentity.js] +"use strict"; +var toThenable = function (fn) { + return function (input) { + var result = fn(input); + return { + then: function (onFulfilled) { + return toThenable(onFulfilled)(result); + } + }; + }; +}; +var toThenableInferred = function (fn) { + return function (input) { + var result = fn(input); + return { + then: function (onFulfilled) { + return toThenable(onFulfilled)(result); + } + }; + }; +}; diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols new file mode 100644 index 0000000000000..f29d97afe28ed --- /dev/null +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols @@ -0,0 +1,104 @@ +//// [tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts] //// + +=== genericCallWithinOwnBodyCastTypeParameterIdentity.ts === +interface Thenable { +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>Value : Symbol(Value, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 19)) + + then( +>then : Symbol(Thenable.then, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 27)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 1, 9)) + + onFulfilled: (value: Value) => V | Thenable, +>onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 1, 12)) +>value : Symbol(value, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 2, 22)) +>Value : Symbol(Value, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 19)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 1, 9)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 1, 9)) + + ): Thenable; +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 1, 9)) +} + +const toThenable = (fn: (input: Input) => Result | Thenable) => +>toThenable : Symbol(toThenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 5)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 27)) +>fn : Symbol(fn, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 35)) +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 40)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 27)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) + + (input: Input): Thenable => { +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 7, 5)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 27)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) + + const result = fn(input) +>result : Symbol(result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 8, 13)) +>fn : Symbol(fn, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 35)) +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 7, 5)) + + return { + then(onFulfilled: (value: Result) => V | Thenable) { +>then : Symbol(then, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 9, 16)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 17)) +>onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 20)) +>value : Symbol(value, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 34)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 17)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 17)) + + return toThenable(onFulfilled)(result as Result) +>toThenable : Symbol(toThenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 5)) +>V : Symbol(V, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 17)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) +>onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 10, 20)) +>result : Symbol(result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 8, 13)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 20)) + } + }; + } + +const toThenableInferred = (fn: (input: Input) => Result | Thenable) => +>toThenableInferred : Symbol(toThenableInferred, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 5)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 35)) +>fn : Symbol(fn, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 43)) +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 48)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 35)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) + + (input: Input): Thenable => { +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 17, 5)) +>Input : Symbol(Input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 35)) +>Thenable : Symbol(Thenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 0, 0)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) + + const result = fn(input) +>result : Symbol(result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 18, 13)) +>fn : Symbol(fn, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 43)) +>input : Symbol(input, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 17, 5)) + + return { + then(onFulfilled) { +>then : Symbol(then, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 19, 16)) +>onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 20, 17)) + + return toThenable(onFulfilled)(result as Result) +>toThenable : Symbol(toThenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 5)) +>onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 20, 17)) +>result : Symbol(result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 18, 13)) +>Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) + } + }; + } + diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types new file mode 100644 index 0000000000000..92ab6d352bc96 --- /dev/null +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types @@ -0,0 +1,83 @@ +//// [tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts] //// + +=== genericCallWithinOwnBodyCastTypeParameterIdentity.ts === +interface Thenable { + then( +>then : (onFulfilled: (value: Value) => V | Thenable) => Thenable + + onFulfilled: (value: Value) => V | Thenable, +>onFulfilled : (value: Value) => V | Thenable +>value : Value + + ): Thenable; +} + +const toThenable = (fn: (input: Input) => Result | Thenable) => +>toThenable : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>(fn: (input: Input) => Result | Thenable) => (input: Input): Thenable => { const result = fn(input) return { then(onFulfilled: (value: Result) => V | Thenable) { return toThenable(onFulfilled)(result as Result) } }; } : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>fn : (input: Input) => Result | Thenable +>input : Input + + (input: Input): Thenable => { +>(input: Input): Thenable => { const result = fn(input) return { then(onFulfilled: (value: Result) => V | Thenable) { return toThenable(onFulfilled)(result as Result) } }; } : (input: Input) => Thenable +>input : Input + + const result = fn(input) +>result : Result | Thenable +>fn(input) : Result | Thenable +>fn : (input: Input) => Result | Thenable +>input : Input + + return { +>{ then(onFulfilled: (value: Result) => V | Thenable) { return toThenable(onFulfilled)(result as Result) } } : { then(onFulfilled: (value: Result) => V | Thenable): Thenable; } + + then(onFulfilled: (value: Result) => V | Thenable) { +>then : (onFulfilled: (value: Result) => V | Thenable) => Thenable +>onFulfilled : (value: Result) => V | Thenable +>value : Result + + return toThenable(onFulfilled)(result as Result) +>toThenable(onFulfilled)(result as Result) : Thenable +>toThenable(onFulfilled) : (input: Result) => Thenable +>toThenable : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>onFulfilled : (value: Result) => V | Thenable +>result as Result : Result +>result : Result | Thenable + } + }; + } + +const toThenableInferred = (fn: (input: Input) => Result | Thenable) => +>toThenableInferred : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>(fn: (input: Input) => Result | Thenable) => (input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } }; } : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>fn : (input: Input) => Result | Thenable +>input : Input + + (input: Input): Thenable => { +>(input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } }; } : (input: Input) => Thenable +>input : Input + + const result = fn(input) +>result : Result | Thenable +>fn(input) : Result | Thenable +>fn : (input: Input) => Result | Thenable +>input : Input + + return { +>{ then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } } : { then(onFulfilled: (value: Result) => V | Thenable): Thenable; } + + then(onFulfilled) { +>then : (onFulfilled: (value: Result) => V | Thenable) => Thenable +>onFulfilled : (value: Result) => V | Thenable + + return toThenable(onFulfilled)(result as Result) +>toThenable(onFulfilled)(result as Result) : Thenable +>toThenable(onFulfilled) : (input: Result) => Thenable +>toThenable : (fn: (input: Input_1) => Result_1 | Thenable) => (input: Input_1) => Thenable +>onFulfilled : (value: Result) => V | Thenable +>result as Result : Result +>result : Result | Thenable + } + }; + } + diff --git a/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts b/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts new file mode 100644 index 0000000000000..e615c33dba4b5 --- /dev/null +++ b/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts @@ -0,0 +1,26 @@ +// @strict: true +interface Thenable { + then( + onFulfilled: (value: Value) => V | Thenable, + ): Thenable; +} + +const toThenable = (fn: (input: Input) => Result | Thenable) => + (input: Input): Thenable => { + const result = fn(input) + return { + then(onFulfilled: (value: Result) => V | Thenable) { + return toThenable(onFulfilled)(result as Result) + } + }; + } + +const toThenableInferred = (fn: (input: Input) => Result | Thenable) => + (input: Input): Thenable => { + const result = fn(input) + return { + then(onFulfilled) { + return toThenable(onFulfilled)(result as Result) + } + }; + } From 17cbccafbf523df5361ddba63a0f23d4a67488aa Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 29 Feb 2024 09:40:15 -0800 Subject: [PATCH 4/4] Fix borked test, re-add type parameter copying --- src/compiler/checker.ts | 27 +++++++++++++++---- src/compiler/types.ts | 2 ++ ...lWithinOwnBodyCastTypeParameterIdentity.js | 4 +-- ...inOwnBodyCastTypeParameterIdentity.symbols | 4 +-- ...thinOwnBodyCastTypeParameterIdentity.types | 14 +++++----- ...lWithinOwnBodyCastTypeParameterIdentity.ts | 2 +- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d307cb7787eaf..0a4be9a95c333 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15632,6 +15632,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { ); } + function getImplementationSignature(signature: Signature) { + return signature.typeParameters ? + signature.implementationSignatureCache ||= createImplementationSignature(signature) : + signature; + } + + function createImplementationSignature(signature: Signature) { + return signature.typeParameters ? instantiateSignature(signature, createTypeMapper([], [])) : signature; + } + function getBaseSignature(signature: Signature) { const typeParameters = signature.typeParameters; if (typeParameters) { @@ -26364,9 +26374,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // inference error only occurs when there are *conflicting* candidates, i.e. // candidates with no common supertype. const defaultType = getDefaultFromTypeParameter(inference.typeParameter); - // Instantiate the default type. Any forward reference to a type - // parameter should be instantiated to the empty object type. - inferredType = instantiateType(defaultType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); + if (defaultType) { + // Instantiate the default type. Any forward reference to a type + // parameter should be instantiated to the empty object type. + inferredType = instantiateType(defaultType, mergeTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); + } } } else { @@ -34867,7 +34879,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { - const candidate = candidates[candidateIndex]; + let candidate = candidates[candidateIndex]; if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { continue; } @@ -34876,6 +34888,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let inferenceContext: InferenceContext | undefined; if (candidate.typeParameters) { + // If we are *inside the body of candidate*, we need to create a clone of `candidate` with differing type parameter identities, + // so our inference results for this call doesn't pollute expression types referencing the outer type parameter! + if (candidate.declaration && findAncestor(node, a => a === candidate.declaration)) { + candidate = getImplementationSignature(candidate); + } let typeArgumentTypes: readonly Type[] | undefined; if (some(typeArguments)) { typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); @@ -34885,7 +34902,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } else { - inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); + inferenceContext = createInferenceContext(candidate.typeParameters!, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); // The resulting type arguments are instantiated with the inference context mapper, as the inferred types may still contain references to the inference context's // type variables via contextual projection. These are kept generic until all inferences are locked in, so the dependencies expressed can pass constraint checks. typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext), inferenceContext.nonFixingMapper); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a56980783e4b8..2c09a3e455b33 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6819,6 +6819,8 @@ export interface Signature { isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison /** @internal */ instantiations?: Map; // Generic signature instantiation cache + /** @internal */ + implementationSignatureCache?: Signature; // Copy of the signature with fresh type parameters to use in checking the body of a potentially self-referential generic function (deferred) } export const enum IndexKind { diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js index b28f5d95fd6b1..9c1884e53c471 100644 --- a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.js @@ -22,7 +22,7 @@ const toThenableInferred = (fn: (input: Input) => Result | Thenab const result = fn(input) return { then(onFulfilled) { - return toThenable(onFulfilled)(result as Result) + return toThenableInferred(onFulfilled)(result as Result) } }; } @@ -45,7 +45,7 @@ var toThenableInferred = function (fn) { var result = fn(input); return { then: function (onFulfilled) { - return toThenable(onFulfilled)(result); + return toThenableInferred(onFulfilled)(result); } }; }; diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols index f29d97afe28ed..7cbd117b09ffb 100644 --- a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.symbols @@ -93,8 +93,8 @@ const toThenableInferred = (fn: (input: Input) => Result | Thenab >then : Symbol(then, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 19, 16)) >onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 20, 17)) - return toThenable(onFulfilled)(result as Result) ->toThenable : Symbol(toThenable, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 6, 5)) + return toThenableInferred(onFulfilled)(result as Result) +>toThenableInferred : Symbol(toThenableInferred, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 5)) >onFulfilled : Symbol(onFulfilled, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 20, 17)) >result : Symbol(result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 18, 13)) >Result : Symbol(Result, Decl(genericCallWithinOwnBodyCastTypeParameterIdentity.ts, 16, 28)) diff --git a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types index 92ab6d352bc96..390177662069d 100644 --- a/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types +++ b/tests/baselines/reference/genericCallWithinOwnBodyCastTypeParameterIdentity.types @@ -49,12 +49,12 @@ const toThenable = (fn: (input: Input) => Result | Thenable(fn: (input: Input) => Result | Thenable) => >toThenableInferred : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable ->(fn: (input: Input) => Result | Thenable) => (input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } }; } : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable +>(fn: (input: Input) => Result | Thenable) => (input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenableInferred(onFulfilled)(result as Result) } }; } : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable >fn : (input: Input) => Result | Thenable >input : Input (input: Input): Thenable => { ->(input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } }; } : (input: Input) => Thenable +>(input: Input): Thenable => { const result = fn(input) return { then(onFulfilled) { return toThenableInferred(onFulfilled)(result as Result) } }; } : (input: Input) => Thenable >input : Input const result = fn(input) @@ -64,16 +64,16 @@ const toThenableInferred = (fn: (input: Input) => Result | Thenab >input : Input return { ->{ then(onFulfilled) { return toThenable(onFulfilled)(result as Result) } } : { then(onFulfilled: (value: Result) => V | Thenable): Thenable; } +>{ then(onFulfilled) { return toThenableInferred(onFulfilled)(result as Result) } } : { then(onFulfilled: (value: Result) => V | Thenable): Thenable; } then(onFulfilled) { >then : (onFulfilled: (value: Result) => V | Thenable) => Thenable >onFulfilled : (value: Result) => V | Thenable - return toThenable(onFulfilled)(result as Result) ->toThenable(onFulfilled)(result as Result) : Thenable ->toThenable(onFulfilled) : (input: Result) => Thenable ->toThenable : (fn: (input: Input_1) => Result_1 | Thenable) => (input: Input_1) => Thenable + return toThenableInferred(onFulfilled)(result as Result) +>toThenableInferred(onFulfilled)(result as Result) : Thenable +>toThenableInferred(onFulfilled) : (input: Result) => Thenable +>toThenableInferred : (fn: (input: Input) => Result | Thenable) => (input: Input) => Thenable >onFulfilled : (value: Result) => V | Thenable >result as Result : Result >result : Result | Thenable diff --git a/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts b/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts index e615c33dba4b5..97058662ae75d 100644 --- a/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts +++ b/tests/cases/compiler/genericCallWithinOwnBodyCastTypeParameterIdentity.ts @@ -20,7 +20,7 @@ const toThenableInferred = (fn: (input: Input) => Result | Thenab const result = fn(input) return { then(onFulfilled) { - return toThenable(onFulfilled)(result as Result) + return toThenableInferred(onFulfilled)(result as Result) } }; }