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

Lazily compute signature type predicates #17600

Merged
22 commits merged into from
Dec 5, 2017
Merged

Lazily compute signature type predicates #17600

22 commits merged into from
Dec 5, 2017

Conversation

ghost
Copy link

@ghost ghost commented Aug 3, 2017

Fixes #17451
EDIT: And #19640 and #19642 and #20186.

To summarize the problem:

During the first quickInfo, we start from a blank slate.

In chooseOverload:

  • First time around, we set excludedArgument to [true], meaning that we exclude the sole argument.
  • Obviously, we get no type inferences from this. So the inferred type argument is {}.
  • Then in the next round, excludeArgument will be undefined. This means we enter the body of checkFunctionExpressionOrObjectLiteralMethod; particularly where we call instantiateSignature(contextualSignature, contextualMapper);.
  • In instantiating the contextual signature, we end up calling an innocent-looking function called cloneTypePredicate. This uses the contextual mapper to call map the contextual type predicate type to the actual type predicate type.
  • By calling the mapper (created by createInferenceContext), we cause a side-effect of fixing the inference -- so it gets stuck at {} forever.

The statefulness in the original issue is because by the second quickInfo, we've already checked the function, and its return type is (n: {}) => n is number. The explicit : n is number annotation overrides the contextual type. So getTypeOfFuncClassEnumModule returns a completed full type just an empty new one, and we can infer types correctly. During the first quickInfo we didn't have a type for the function yet, so type inference problems only showed up then.

We had already solved this sort of problem for contextual return types: compute them lazily so that we don't call the contextual mapper until necessary. So the solution is simply to do the same for the type predicate.

@ghost ghost requested review from sandersn and ahejlsberg August 3, 2017 22:25
@ghost
Copy link
Author

ghost commented Aug 3, 2017

For posterity, the test I was using to reproduce #17451 was:

///<reference path="fourslash.ts"/>

// @noLib: true

////declare function f<T>(predicate: (t: {}) => t is T): T;
////const y = /**/f((n): n is number => true);

verify.quickInfoAt("", "function f<{}>(predicate: (t: {}) => t is {}): {}"); // OK

console.log("First succeeded...");

verify.quickInfoAt("", "function f<{}>(predicate: (t: {}) => t is {}): {}"); // Fails

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I like the delay -- it should have always been this way to mirror the way return types are treated -- but I wish there were a more uniform way to represent 'pending' vs 'missing'. The problem is that for efficiency, the common case should be 'undefined', which for type predicates is 'missing' and for return types is 'pending'.

}

function getTypePredicateOfSignature(signature: Signature): TypePredicate {
if (signature.resolvedTypePredicate === "pending") {
Copy link
Member

Choose a reason for hiding this comment

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

why do we need a third state when resolvedReturnType doesn't?

Copy link
Member

Choose a reason for hiding this comment

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

oh, because all signatures have a return type (eventually), but not all signatures will have a typePredicate. So "pending" for resolvedReturnType is the equivalent of undefined for resolvedTypePredicate.

Copy link
Member

Choose a reason for hiding this comment

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

oh, this is documented in the definition in types.ts

@@ -5434,14 +5435,14 @@ namespace ts {
}

function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], thisParameter: Symbol | undefined, parameters: Symbol[],
resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasLiteralTypes: boolean): Signature {
resolvedReturnType: Type | undefined, resolvedTypePredicate: TypePredicate | undefined | "pending", minArgumentCount: number, hasRestParameter: boolean, hasLiteralTypes: boolean): Signature {
Copy link
Member

Choose a reason for hiding this comment

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

Please be aware that if we call createSignature once with a TypePredicate and then call createSignature again later with "pending" that it will result in a V8 compiling a second copy of createSignature due to the parameter being polymorphic (string vs. object).

We also end up making Signature more polymorphic. This could result in degraded performance so I would recommend you run benchmarks before and after this change to ensure this does not cause a regression.

As an alternative, I would suggest that you define something like a const unresolvedTypePredicate = <IdentifierTypePredicate>{ kind: TypePredicateKind.Identifier, parameterName: "pending", parameterIndex: 0 }; to act as your "pending" sentinel value as it will reduce polymorphism.

Copy link
Author

Choose a reason for hiding this comment

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

Could we just use null for pending?

Copy link
Member

Choose a reason for hiding this comment

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

We try not to use null in the compiler at all, and it would be inconsistent.

@@ -280,6 +280,11 @@ namespace ts {
const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);

const unresolvedTypePredicate: void & { __unresolvedTypePredicate: void } = (() => {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this type so needlessly complex? Just use TypePredicate.

Copy link
Author

Choose a reason for hiding this comment

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

I want to ensure that this is definitely not useable as a TypePredicate -- it's just one to prevent polymorphism. Otherwise it's too easy to access resolvedTypePredicate and think you're getting the right thing.

Copy link
Member

Choose a reason for hiding this comment

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

We generally don't have that issue with other cases like noConstraintType, especially if all access to the type predicate is gated through getTypePredicateOfSignature.

While I don't think it's strictly necessary, you could add another TypePredicateKind as a discriminant, but I think using an object whose shape (and hidden class) matches other valid values for that property will reduce polymorphism.

Copy link
Author

@ghost ghost Aug 9, 2017

Choose a reason for hiding this comment

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

I would disagree that it's not an issue -- it might not be an issue if you wrote the code itself, but coming from the outside, this was a barrier to my understanding this code, and it would have been easier had I realized that resolvedReturnType was not meant to be used directly as a Type. I hadn't come across noConstraintType yet, but I'm sure it wouldn't have been easy to realize that a variable of type Type should not actually be used as a Type because it might be a special sentinel value.

Copy link
Member

Choose a reason for hiding this comment

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

This one of many cases where we need better documentation in checker.

Copy link
Member

Choose a reason for hiding this comment

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

I would prefer we stick to our current pattern for these cases. We can discuss these specific concerns with the broader team following the 2.5 release.

@@ -11403,7 +11423,7 @@ namespace ts {
const apparentType = getApparentType(funcType);
if (apparentType !== unknownType) {
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
return !!forEach(callSignatures, sig => sig.typePredicate);
return some(callSignatures, sig => signatureHasTypePredicate(sig));
Copy link
Member

Choose a reason for hiding this comment

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

Can you just use signatureHasTypePredicate directly instead?

Copy link
Author

Choose a reason for hiding this comment

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

👍

return signature.resolvedTypePredicate !== undefined;
}

function getTypePredicateOfSignature(signature: Signature): TypePredicate | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

Another approach we could consider is the one used to resolve Type Parameter constraints. Instead of using undefined to indicate no type predicate and an unresolvedTypePredicate sentinel to indicate deferred resolution, use undefined to indicate deferred resolution and a noTypePredicate sentinel. See noConstraintType as an example.

Copy link
Author

Choose a reason for hiding this comment

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

Shouldn't undefined be used for the most common case though? Usually no type predicate exists.

Copy link
Member

Choose a reason for hiding this comment

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

It can also mean that we don't know the answer yet. This is consistent with other "resolve"-named properties used in checker.

Copy link
Author

Choose a reason for hiding this comment

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

I've inverted the representation, so undefined now represents a lazily-computed type predicate and noTypePredicate represents no type predicate existing.

@rbuckton
Copy link
Member

rbuckton commented Aug 9, 2017

Can you take a look at the build failures?

Andy Hanson added 2 commits August 9, 2017 10:45
@ghost
Copy link
Author

ghost commented Aug 29, 2017

@rbuckton @ahejlsberg Any more comments?

@ghost
Copy link
Author

ghost commented Sep 13, 2017

Bump -- @rbuckton @ahejlsberg Is anything blocking this?

@ghost
Copy link
Author

ghost commented Sep 22, 2017

@rbuckton Good to go?

signature.resolvedTypePredicate = instantiateTypePredicate(getTypePredicateOfSignature(signature.target), signature.mapper);
}
return signature.resolvedTypePredicate as TypePredicate;
}
Copy link
Member

Choose a reason for hiding this comment

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

Write this function in the same style as getConstraintFromTypeParameter.

@ghost
Copy link
Author

ghost commented Oct 6, 2017

New commit contains fixes to the union half of #17757. Intersecting type predicates is marked as a TODO.
Previously we considered signatures identical even if they had different type predicates; this meant a union of two different type predicates would just contain the first one.
@sandersn @ahejlsberg Needs new review for these new changes.

@ghost
Copy link
Author

ghost commented Oct 20, 2017

@sandersn @rbuckton @ahejlsberg Anyone?

@ghost ghost force-pushed the typepredicate branch from 3d9ad82 to b384c9c Compare October 20, 2017 15:42
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

One test assertion in a comment can be checked by the compiler. Otherwise looks good.

@@ -0,0 +1,3 @@
declare function f<T>(predicate: (x: {}) => x is T): T;
// 'res' should be of type 'number'.
Copy link
Member

Choose a reason for hiding this comment

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

change this line to var res: number and start the next line with var res = ... and the compiler will enforce this assertion.

Copy link
Author

@ghost ghost Oct 20, 2017

Choose a reason for hiding this comment

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

I didn't want us taking any contextual type -- presumably we should be doing that on assignments if we're not already.
The baselines do enforce this assertion.

type Or = A | B;

function f(o: Or, x: {}, y: {}) {
if (o.pred(x, y)) {
Copy link
Member

Choose a reason for hiding this comment

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

interesting.. why doesn't this match?

Copy link
Member

Choose a reason for hiding this comment

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

oh, it's because you can't make a single type predicate that narrows two variables.

// Lazily set by `getTypePredicateOfSignature`.
// `undefined` indicates a type predicate that has not yet been computed.
// Uses a special `noTypePredicate` sentinel value to indicate that there is no type predicate. This looks like a TypePredicate at runtime to avoid polymorphism.
// (See https://github.com/Microsoft/TypeScript/pull/17600#discussion_r132059173)
Copy link
Member

Choose a reason for hiding this comment

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

This link is a bet that our use of github will last as long as the typescript source. Probably a decent bet, but I personally think the explanation stands on its own as a valid justification.

(Same for the use of github bug numbers earlier in the review.)

sig.minArgumentCount = minArgumentCount;
sig.hasRestParameter = hasRestParameter;
sig.hasLiteralTypes = hasLiteralTypes;
return sig;
}

function cloneSignature(sig: Signature): Signature {
return createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, sig.resolvedReturnType,
sig.typePredicate, sig.minArgumentCount, sig.hasRestParameter, sig.hasLiteralTypes);
return createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, /*resolvedReturnType*/ undefined,
Copy link
Member

Choose a reason for hiding this comment

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

odd, I thought cloneSignature already zeroed out resolvedReturnType.

Copy link
Member

Choose a reason for hiding this comment

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

oh, right, this is from that analysis you did which showed that all callers of cloneSignature immediately zeroed it out themselves.

function getUnionTypePredicate(signatures: ReadonlyArray<Signature>): TypePredicate {
let first: TypePredicate | undefined;
const types: Type[] = [];
for (let i = 0; i < signatures.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

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

why not for-of here?

Copy link
Author

Choose a reason for hiding this comment

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

If we did #19309 we could enable prefer-for-of to catch this kind of error.

getIndexTypeOfStructuredType,
getConstraintFromTypeParameter,
getFirstIdentifier,
),
Copy link
Member

Choose a reason for hiding this comment

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

Did anything change here? Otherwise, just leave it alone.

hasRestParameter,
hasLiteralTypes) {
return createSignature(declaration, typeParameters, thisParameter, parameters, resolvedReturnType, resolvedTypePredicate || noTypePredicate, minArgumentCount, hasRestParameter, hasLiteralTypes);
},
Copy link
Member

Choose a reason for hiding this comment

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

Why the change here?

const sig = new Signature(checker);
sig.declaration = declaration;
sig.typeParameters = typeParameters;
sig.parameters = parameters;
sig.thisParameter = thisParameter;
sig.resolvedReturnType = resolvedReturnType;
sig.typePredicate = typePredicate;
sig.resolvedTypePredicate = resolvedTypePredicate;
Copy link
Member

Choose a reason for hiding this comment

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

I would just do typePredicate || noTypePredicate here and then you won't need all the other changes.

@ahejlsberg
Copy link
Member

ahejlsberg commented Oct 25, 2017

I just pushed a branch with some suggested changes:

https://github.com/Microsoft/TypeScript/tree/typePredicateChanges

This makes the noTypePredicate sentinel purely an implementation detail of getTypePredicateOfSignature and aligns better with how getReturnTypeOfSignature works.

@ghost
Copy link
Author

ghost commented Nov 1, 2017

@ahejlsberg Good to go?

@ahejlsberg
Copy link
Member

@andy-ms Also, check to see if this fixes #19676.

@ghost
Copy link
Author

ghost commented Nov 21, 2017

#19676 is fixed, tested for by typeInferenceTypePredicate.ts. (And tested with local build.)

@@ -866,9 +866,9 @@ define(function () {
>'UnknownMobile' : "UnknownMobile"

isArray = ('isArray' in Array) ?
>isArray = ('isArray' in Array) ? Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : (arg: any) => arg is any[]
>isArray = ('isArray' in Array) ? Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : (value: any) => boolean
Copy link
Author

Choose a reason for hiding this comment

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

I think this change is correct because only the left hand side of the conditional is a type predicate. @ahejlsberg could you verify, then merge

Copy link
Member

Choose a reason for hiding this comment

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

Something seems fishy here. If you reverse the order of the operands you get the old result. So apparently both types are subtypes of each other, or otherwise subtype reduction would consistently pick one of them. I'm not sure why this is the case, since (x: any) => x is any[] should be a subtype of (x: any) => boolean. You may want to investigate.

Copy link
Author

Choose a reason for hiding this comment

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

Added a test -- the return type seems to be boolean the other way around as well.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, looks good.

@ahejlsberg
Copy link
Member

I think this PR is good to go.

@ahejlsberg ahejlsberg added this to the TypeScript 2.7 milestone Dec 3, 2017
@ghost ghost merged commit 8153397 into master Dec 5, 2017
@ghost ghost deleted the typepredicate branch December 5, 2017 16:32
trixnz added a commit to trixnz/vscode-lua that referenced this pull request Dec 7, 2017
Note: This includes a workaround to a type inferring issue fixed by microsoft/TypeScript#17600. When Typescript 2.7 is released, these forms can be removed.
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Strange stateful behavior with inferred type predicate type
5 participants