Skip to content
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

Make sure type parameters stay fixed throughout the inference process #2356

Merged
merged 12 commits into from
Mar 17, 2015
Merged
Show file tree
Hide file tree
Changes from 10 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
109 changes: 73 additions & 36 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ module ts {
let emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
let anyFunctionType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
let noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
let inferenceFailureType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);


let anySignature = createSignature(undefined, undefined, emptyArray, anyType, 0, false, false);
let unknownSignature = createSignature(undefined, undefined, emptyArray, unknownType, 0, false, false);

Expand Down Expand Up @@ -3514,6 +3513,7 @@ module ts {
return t => {
for (let i = 0; i < context.typeParameters.length; i++) {
if (t === context.typeParameters[i]) {
context.inferences[i].isFixed = true;
return getInferredType(context, i);
}
}
Expand Down Expand Up @@ -4388,6 +4388,8 @@ module ts {
}
}

Debug.assert(!!downfallType, "If there is no common supertype, each type should have a downfallType");
Copy link
Member

Choose a reason for hiding this comment

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

downfallType? Is this supposed to be like a fallbackType? If so, rename it. downfallType sounds like it describes revolutions, lost wars, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is supposed to be downfallType. It is supposed to sound like it describes revolutions and lost wars, but in particular tragic heroes.

The downfallType is the type that caused a particular candidate to not be the common supertype. So if it weren't for this one downfallType, the type in question could have been the common supertype.

Note that downfallType is the first type that is not a subtype of type. It's not necessarily the only one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add this explanation in the code

The downfallType is the type that caused a particular candidate to not be the common supertype. So if it weren't for this one downfallType, the type in question could have been the common supertype.


if (score > bestSupertypeScore) {
bestSupertype = types[i];
bestSupertypeDownfallType = downfallType;
Expand Down Expand Up @@ -4570,12 +4572,11 @@ module ts {
function createInferenceContext(typeParameters: TypeParameter[], inferUnionTypes: boolean): InferenceContext {
let inferences: TypeInferences[] = [];
for (let unused of typeParameters) {
inferences.push({ primary: undefined, secondary: undefined });
inferences.push({ primary: undefined, secondary: undefined, isFixed: false });
}
return {
typeParameters: typeParameters,
inferUnionTypes: inferUnionTypes,
inferenceCount: 0,
inferences: inferences,
inferredTypes: new Array(typeParameters.length),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this have failedTypeParameterIndex as a property? It is optional though I find it will be easier to read through

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I'm going to leave it out. The reason it's optional is because it is always initially undefined, and undefined means that there have been no failures (yet).

};
Expand Down Expand Up @@ -4622,11 +4623,21 @@ module ts {
for (let i = 0; i < typeParameters.length; i++) {
if (target === typeParameters[i]) {
let inferences = context.inferences[i];
let candidates = inferiority ?
inferences.secondary || (inferences.secondary = []) :
inferences.primary || (inferences.primary = []);
if (!contains(candidates, source)) candidates.push(source);
break;
if (!inferences.isFixed) {
// Any inferences that are made to a type parameter in a union type are inferior
// to inferences made to a flat (non-union) type. This is because if we infer to
// T | string[], we really don't know if we should be inferring to T or not (because
// the correct constituent on the target side could be string[]). Therefore, we put
// such inferior inferences into a secondary bucket, and only use them if the primary
// bucket is empty.
let candidates = inferiority ?
Copy link
Member

Choose a reason for hiding this comment

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

There's no documentation for inferiority.

Copy link
Member

Choose a reason for hiding this comment

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

Also, the documentation for primary and secondary are a bit lacking; elaborate on the distinctions and why we make these distinctions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// Any inferences that are made to a type parameter in a union type are inferior
// to inferences made to a flat (non-union) type. This is because if we infer to
// T | string[], we really don't know if we should be inferring to T or not (because
// the correct constituent on the target side could be string[]). Therefore, we put
// such inferior inferences into a secondary bucket, and only use them if the primary
// bucket is empty.

Copy link
Member

Choose a reason for hiding this comment

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

👏

inferences.secondary || (inferences.secondary = []) :
inferences.primary || (inferences.primary = []);
if (!contains(candidates, source)) {
candidates.push(source);
}
}
return;
}
}
}
Expand Down Expand Up @@ -4732,21 +4743,35 @@ module ts {

function getInferredType(context: InferenceContext, index: number): Type {
let inferredType = context.inferredTypes[index];
let inferenceSucceeded: boolean;
if (!inferredType) {
let inferences = getInferenceCandidates(context, index);
if (inferences.length) {
// Infer widened union or supertype, or the undefined type for no common supertype
// Infer widened union or supertype, or the unknown type for no common supertype
let unionOrSuperType = context.inferUnionTypes ? getUnionType(inferences) : getCommonSupertype(inferences);
inferredType = unionOrSuperType ? getWidenedType(unionOrSuperType) : inferenceFailureType;
inferredType = unionOrSuperType ? getWidenedType(unionOrSuperType) : unknownType;
inferenceSucceeded = !!unionOrSuperType;
}
else {
// Infer the empty object type when no inferences were made
// Infer the empty object type when no inferences were made. It is important to remember that
// in this case, inference still succeeds, meaning there is no error for not having inference
// candidates. An inference error only occurs when there are *conflicting* candidates, i.e.
// candidates with no common supertype.
inferredType = emptyObjectType;
inferenceSucceeded = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems odd. No inferences were made, but inference succeeded? Can you provide some some explnatories comments on this? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, but this is just a rule of the language. If there were no inferences, inference still succeeds. I don't love it. But @DanielRosenwasser has a PR where he issues an error for this case under a compiler flag.

Copy link
Member

Choose a reason for hiding this comment

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

It's not yet a PR; I'd like to make it a PR if we could solve some of the original concerns in #360.

To clarify, when inference for a type argument fails, the process falls back to using {} as a type argument. 👍 for explaining what a failure scenario would be in contrast to no inferences being found.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'll add a comment.

}
if (inferredType !== inferenceFailureType) {

// Only do the constraint check if inference succeeded (to prevent cascading errors)
Copy link
Contributor

Choose a reason for hiding this comment

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

Great comment, thanks.

if (inferenceSucceeded) {
let constraint = getConstraintOfTypeParameter(context.typeParameters[index]);
inferredType = constraint && !isTypeAssignableTo(inferredType, constraint) ? constraint : inferredType;
}
else if (context.failedTypeParameterIndex === undefined || context.failedTypeParameterIndex > index) {
// If inference failed, it is necessary to record the index of the failed type parameter (the one we are on).
// It might be that inference has already failed on a later type parameter on a previous call to inferTypeArguments.
// So if this failure is on preceding type parameter, this type parameter is the new failure index.
context.failedTypeParameterIndex = index;
}
context.inferredTypes[index] = inferredType;
}
return inferredType;
Expand Down Expand Up @@ -6343,11 +6368,32 @@ module ts {
return getSignatureInstantiation(signature, getInferredTypes(context));
}

function inferTypeArguments(signature: Signature, args: Expression[], excludeArgument: boolean[]): InferenceContext {
function inferTypeArguments(signature: Signature, args: Expression[], excludeArgument: boolean[], context: InferenceContext): void {
let typeParameters = signature.typeParameters;
let context = createInferenceContext(typeParameters, /*inferUnionTypes*/ false);
let inferenceMapper = createInferenceMapper(context);

// Clear out all the inference results from the last time inferTypeArguments was called on this context
for (let i = 0; i < typeParameters.length; i++) {
// As an optimization, we don't have to clear (and later recompute) inferred types
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually an optimization? Or is this necessary to get correct results? I thought it was the latter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not an optimization that we reset it. It's an optimization that we skip resetting it if the type parameter was already fixed. It would be just as correct to reset all of them. But then we'd be repeating the same work for the type parameters that were fixed. Namely the work done by getInferredType.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good to explain in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'm basically adding my comment from above as a comment in the code

// for type parameters that have already been fixed on the previous call to inferTypeArguments.
// It would be just as correct to reset all of them. But then we'd be repeating the same work
// for the type parameters that were fixed, namely the work done by getInferredType.
if (!context.inferences[i].isFixed) {
context.inferredTypes[i] = undefined;
}
}

// On this call to inferTypeArguments, we may get more inferences for certain type parameters that were not
// fixed last time. This means that a type parameter that failed inference last time may succeed this time,
// or vice versa. Therefore, the failedTypeParameterIndex is useless if it points to an unfixed type parameter,
// because it may change. So here we reset it. However, getInferredType will not revisit any type parameters
// that were previously fixed. So if a fixed type parameter failed previously, it will fail again because
// it will contain the exact same set of inferences. So if we reset the index from a fixed type parameter,
// we will lose information that we won't recover this time around.
if (context.failedTypeParameterIndex !== undefined && !context.inferences[context.failedTypeParameterIndex].isFixed) {
context.failedTypeParameterIndex = undefined;
}

// We perform two passes over the arguments. In the first pass we infer from all arguments, but use
// wildcards for all context sensitive function expressions.
for (let i = 0; i < args.length; i++) {
Expand Down Expand Up @@ -6382,18 +6428,7 @@ module ts {
}
}

let inferredTypes = getInferredTypes(context);
// Inference has failed if the inferenceFailureType type is in list of inferences
context.failedTypeParameterIndex = indexOf(inferredTypes, inferenceFailureType);

// Wipe out the inferenceFailureType from the array so that error recovery can work properly
for (let i = 0; i < inferredTypes.length; i++) {
if (inferredTypes[i] === inferenceFailureType) {
inferredTypes[i] = unknownType;
}
}

return context;
getInferredTypes(context);
}

function checkTypeArguments(signature: Signature, typeArguments: TypeNode[], typeArgumentResultTypes: Type[], reportErrors: boolean): boolean {
Expand Down Expand Up @@ -6627,15 +6662,17 @@ module ts {
return resolveErrorCall(node);

function chooseOverload(candidates: Signature[], relation: Map<RelationComparisonResult>) {
for (let current of candidates) {
if (!hasCorrectArity(node, args, current)) {
for (let originalCandidate of candidates) {
if (!hasCorrectArity(node, args, originalCandidate)) {
continue;
}

let originalCandidate = current;
let inferenceResult: InferenceContext;

let candidate: Signature;
let typeArgumentsAreValid: boolean;
let inferenceContext = originalCandidate.typeParameters
? createInferenceContext(originalCandidate.typeParameters, /*inferUnionTypes*/ false)
: undefined;

while (true) {
candidate = originalCandidate;
if (candidate.typeParameters) {
Expand All @@ -6645,9 +6682,9 @@ module ts {
typeArgumentsAreValid = checkTypeArguments(candidate, typeArguments, typeArgumentTypes, /*reportErrors*/ false)
}
else {
inferenceResult = inferTypeArguments(candidate, args, excludeArgument);
typeArgumentsAreValid = inferenceResult.failedTypeParameterIndex < 0;
typeArgumentTypes = inferenceResult.inferredTypes;
inferTypeArguments(candidate, args, excludeArgument, inferenceContext);
typeArgumentsAreValid = inferenceContext.failedTypeParameterIndex === undefined;
typeArgumentTypes = inferenceContext.inferredTypes;
}
if (!typeArgumentsAreValid) {
break;
Expand Down Expand Up @@ -6677,7 +6714,7 @@ module ts {
else {
candidateForTypeArgumentError = originalCandidate;
if (!typeArguments) {
resultOfFailedInference = inferenceResult;
resultOfFailedInference = inferenceContext;
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,11 +1487,15 @@ module ts {
(t: Type): Type;
}

// @internal
export interface TypeInferences {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might want to consider marking this as @internal so it doesn't affect the API tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should just add a keyword for this instead of having this comment annotation thing. But sure.

primary: Type[]; // Inferences made directly to a type parameter
secondary: Type[]; // Inferences made to a type parameter in a union type
isFixed: boolean; // Whether the type parameter is fixed, as defined in section 4.12.2 of the TypeScript spec
// If a type parameter is fixed, no more inferences can be made for the type parameter
}

// @internal
export interface InferenceContext {
typeParameters: TypeParameter[]; // Type parameters for which inferences are made
inferUnionTypes: boolean; // Infer union types for disjoint candidates (otherwise undefinedType)
Expand Down
11 changes: 0 additions & 11 deletions tests/baselines/reference/APISample_compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1176,17 +1176,6 @@ declare module "typescript" {
interface TypeMapper {
(t: Type): Type;
}
interface TypeInferences {
primary: Type[];
secondary: Type[];
}
interface InferenceContext {
typeParameters: TypeParameter[];
inferUnionTypes: boolean;
inferences: TypeInferences[];
inferredTypes: Type[];
failedTypeParameterIndex?: number;
}
interface DiagnosticMessage {
key: string;
category: DiagnosticCategory;
Expand Down
32 changes: 0 additions & 32 deletions tests/baselines/reference/APISample_compile.types
Original file line number Diff line number Diff line change
Expand Up @@ -3769,38 +3769,6 @@ declare module "typescript" {
>t : Type
>Type : Type
>Type : Type
}
interface TypeInferences {
>TypeInferences : TypeInferences

primary: Type[];
>primary : Type[]
>Type : Type

secondary: Type[];
>secondary : Type[]
>Type : Type
}
interface InferenceContext {
>InferenceContext : InferenceContext

typeParameters: TypeParameter[];
>typeParameters : TypeParameter[]
>TypeParameter : TypeParameter

inferUnionTypes: boolean;
>inferUnionTypes : boolean

inferences: TypeInferences[];
>inferences : TypeInferences[]
>TypeInferences : TypeInferences

inferredTypes: Type[];
>inferredTypes : Type[]
>Type : Type

failedTypeParameterIndex?: number;
>failedTypeParameterIndex : number
}
interface DiagnosticMessage {
>DiagnosticMessage : DiagnosticMessage
Expand Down
11 changes: 0 additions & 11 deletions tests/baselines/reference/APISample_linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1207,17 +1207,6 @@ declare module "typescript" {
interface TypeMapper {
(t: Type): Type;
}
interface TypeInferences {
primary: Type[];
secondary: Type[];
}
interface InferenceContext {
typeParameters: TypeParameter[];
inferUnionTypes: boolean;
inferences: TypeInferences[];
inferredTypes: Type[];
failedTypeParameterIndex?: number;
}
interface DiagnosticMessage {
key: string;
category: DiagnosticCategory;
Expand Down
32 changes: 0 additions & 32 deletions tests/baselines/reference/APISample_linter.types
Original file line number Diff line number Diff line change
Expand Up @@ -3915,38 +3915,6 @@ declare module "typescript" {
>t : Type
>Type : Type
>Type : Type
}
interface TypeInferences {
>TypeInferences : TypeInferences

primary: Type[];
>primary : Type[]
>Type : Type

secondary: Type[];
>secondary : Type[]
>Type : Type
}
interface InferenceContext {
>InferenceContext : InferenceContext

typeParameters: TypeParameter[];
>typeParameters : TypeParameter[]
>TypeParameter : TypeParameter

inferUnionTypes: boolean;
>inferUnionTypes : boolean

inferences: TypeInferences[];
>inferences : TypeInferences[]
>TypeInferences : TypeInferences

inferredTypes: Type[];
>inferredTypes : Type[]
>Type : Type

failedTypeParameterIndex?: number;
>failedTypeParameterIndex : number
}
interface DiagnosticMessage {
>DiagnosticMessage : DiagnosticMessage
Expand Down
11 changes: 0 additions & 11 deletions tests/baselines/reference/APISample_transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1208,17 +1208,6 @@ declare module "typescript" {
interface TypeMapper {
(t: Type): Type;
}
interface TypeInferences {
primary: Type[];
secondary: Type[];
}
interface InferenceContext {
typeParameters: TypeParameter[];
inferUnionTypes: boolean;
inferences: TypeInferences[];
inferredTypes: Type[];
failedTypeParameterIndex?: number;
}
interface DiagnosticMessage {
key: string;
category: DiagnosticCategory;
Expand Down
32 changes: 0 additions & 32 deletions tests/baselines/reference/APISample_transform.types
Original file line number Diff line number Diff line change
Expand Up @@ -3865,38 +3865,6 @@ declare module "typescript" {
>t : Type
>Type : Type
>Type : Type
}
interface TypeInferences {
>TypeInferences : TypeInferences

primary: Type[];
>primary : Type[]
>Type : Type

secondary: Type[];
>secondary : Type[]
>Type : Type
}
interface InferenceContext {
>InferenceContext : InferenceContext

typeParameters: TypeParameter[];
>typeParameters : TypeParameter[]
>TypeParameter : TypeParameter

inferUnionTypes: boolean;
>inferUnionTypes : boolean

inferences: TypeInferences[];
>inferences : TypeInferences[]
>TypeInferences : TypeInferences

inferredTypes: Type[];
>inferredTypes : Type[]
>Type : Type

failedTypeParameterIndex?: number;
>failedTypeParameterIndex : number
}
interface DiagnosticMessage {
>DiagnosticMessage : DiagnosticMessage
Expand Down
Loading