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

feat: dependent contextual inference #51511

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

devanshj
Copy link

@devanshj devanshj commented Nov 13, 2022

Fixes #51377
Fixes #40439

This PR adds support for what I'm calling "dependent contextual inference"... Sometimes the type for a value depends on the value itself (see linked issues), in those cases the contextual type of the value becomes something like T extends F<T>. Here what you'd want to do is first check the value against the contextual type T extends F<T> (which is what already happens), let's say the type of the value was inferrred to be X, and then (this is the new part) you do a second check against the contextual type T extends F<X> and the result of that would be the final contextually inferred type of the value

Consider this PR a shabby experiment, I want to first get a green light if we want to do something like this, if yes then I can work on it further.

If ya'll prefer me opening an issue proposing this change and discussing it there first then let me know. Types like T extends F<T> pop up when you're typing DSLs and it'd be really great if we could support them better.

Also note that most of this is a sophisticated guesswork, I knew what I wanted to do but I don't know much about the codebase or compilers xD, so there might be better ways to write it (I'll leave some review comments).

Also leaving some todos:

  • Support all values and not just object literals
  • Support all kinds of circular type parameters, ie T extends <any-expression-that-has-T>, not just T extends F<T>. And perhaps even <any-expression-that-has-T>, where T extends <any-other-expression-that-has-T>.
    • T extends <any-expression-that-has-T>
    • <any-expression-that-has-T>, where T extends <any-other-expression-that-has-T>
  • Multiple passes? I think in some cases two passes won't suffice, in theory you'd probably keep doing checks till you get the same type back

Edit: I'm working on this further but first only view the first commit to get a rough enough idea of what's happening.

Comment on lines 28436 to 28439
instantiateType(
contextualType.immediateBaseConstraint,
createTypeMapper([contextualType], [valueType])
);
Copy link
Author

@devanshj devanshj Nov 13, 2022

Choose a reason for hiding this comment

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

This doesn't seem to replace the types in default parameters in a type alias, which is not what I would have expected. That is, extracting out Exclude<keyof Self, "k" | "t"> to a default type parameter K (as I did in #40439) makes the test fail even though it doesn't really change anything.

Comment on lines 28440 to 28442
if (newContextualType.immediateBaseConstraint!.flags & TypeFlags.StructuredType) {
newContextualType.immediateBaseConstraint = resolveStructuredTypeMembers(newContextualType.immediateBaseConstraint as StructuredType);
}
Copy link
Author

@devanshj devanshj Nov 13, 2022

Choose a reason for hiding this comment

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

I don't know why this is needed, but given it is, how do you resolve any kind of type and not just structured?

Comment on lines 28445 to 28453
let nodeLinks = getNodeLinks(node);
nodeLinks.flags &= ~NodeCheckFlags.TypeChecked;
nodeLinks.flags &= ~NodeCheckFlags.ContextChecked;
nodeLinks.resolvedType = undefined;
nodeLinks.resolvedEnumType = undefined;
nodeLinks.resolvedSignature = undefined;
nodeLinks.resolvedSymbol = undefined;
nodeLinks.resolvedIndexInfo = undefined;
nodeLinks.contextFreeType = undefined
Copy link
Author

Choose a reason for hiding this comment

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

I don't know if I'm missing something here or I have something extra here, basically I want to invalidate the previous check.

@devanshj devanshj marked this pull request as draft November 13, 2022 23:37
Comment on lines 28424 to 28426
contextualType.immediateBaseConstraint.aliasTypeArguments &&
contextualType.immediateBaseConstraint.aliasTypeArguments.length === 1 &&
contextualType.immediateBaseConstraint.aliasTypeArguments[0].id === contextualType.id;
Copy link
Author

@devanshj devanshj Nov 13, 2022

Choose a reason for hiding this comment

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

How do you check if type A is dependent on type B? That is I want to write doesTypeDependOn(contextualType.immediateBaseConstraint, contextualType).

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps you want to use isTypeParameterPossiblyReferenced or a similar thing?

Copy link
Author

Choose a reason for hiding this comment

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

That probably requires the node to be checked (which we haven't yet), but even it didn't we want something like (a: Type, b: TypeParameter) => boolean

Copy link
Author

@devanshj devanshj Nov 15, 2022

Choose a reason for hiding this comment

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

Actually I realised not only it would be expensive but also I don't need a function that look ups the type tree of A and finds if it has B, I want a function that looks up the syntax tree of A's declaration and see if there is a reference to B. So I want something roughly like...

const isContextualTypeDependent =
  contextualType &&
  (contextualType & TypeFlags.TypeParameter) &&
  contextualType.symbol.declarations[0].constraint &&
  !!forEachChildRecursively(contextualType.symbol.declarations[0].constraint, node => {
    if (
      node.kind === SyntaxKind.TypeReference &&
      getSymbolOfNode(node.typeName).id ===
      getSymbolOfNode(contextualType.symbol.declarations[0]).id
    ) {
      return true
    }
  })

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Nov 13, 2022

type F<T> =
{ a: unknown
, b: (a: T["a" & keyof T]) => unknown
Copy link
Author

Choose a reason for hiding this comment

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

This test fails if I write T extends { a: infer X } ? X : never (ie as it's written in #51377) instead of T["a" & keyof T], which is something I would have not expected as both versions essentially do the same thing.

@RyanCavanaugh
Copy link
Member

Just curious:

@typescript-bot perf test this faster

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 16, 2022

Heya @RyanCavanaugh, I've started to run the abridged perf test suite on this PR at 9d516b6. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..51511

Metric main 51511 Delta Best Worst
Angular - node (v16.17.1, x64)
Memory used 340,501k (± 0.03%) 340,500k (± 0.03%) -1k (- 0.00%) 340,361k 340,661k
Parse Time 1.89s (± 0.37%) 1.90s (± 0.61%) +0.01s (+ 0.32%) 1.86s 1.91s
Bind Time 0.66s (± 0.76%) 0.65s (± 0.51%) -0.00s (- 0.76%) 0.64s 0.66s
Check Time 5.17s (± 0.34%) 5.19s (± 0.57%) +0.02s (+ 0.41%) 5.12s 5.24s
Emit Time 5.11s (± 0.78%) 5.15s (± 0.89%) +0.04s (+ 0.76%) 5.05s 5.27s
Total Time 12.83s (± 0.43%) 12.89s (± 0.59%) +0.06s (+ 0.44%) 12.68s 13.07s
Compiler-Unions - node (v16.17.1, x64)
Memory used 188,583k (± 0.63%) 188,248k (± 0.65%) -336k (- 0.18%) 186,497k 189,959k
Parse Time 0.79s (± 0.60%) 0.80s (± 0.60%) +0.01s (+ 1.27%) 0.79s 0.81s
Bind Time 0.42s (± 0.79%) 0.42s (± 0.53%) +0.00s (+ 0.24%) 0.42s 0.43s
Check Time 6.04s (± 0.49%) 6.03s (± 0.35%) -0.02s (- 0.25%) 5.98s 6.06s
Emit Time 1.90s (± 1.27%) 1.88s (± 0.68%) -0.01s (- 0.63%) 1.85s 1.91s
Total Time 9.15s (± 0.52%) 9.13s (± 0.19%) -0.02s (- 0.20%) 9.09s 9.16s
Monaco - node (v16.17.1, x64)
Memory used 319,813k (± 0.00%) 319,826k (± 0.01%) +13k (+ 0.00%) 319,774k 319,854k
Parse Time 1.42s (± 0.58%) 1.43s (± 0.79%) +0.00s (+ 0.28%) 1.39s 1.44s
Bind Time 0.59s (± 0.57%) 0.59s (± 0.80%) -0.00s (- 0.00%) 0.58s 0.60s
Check Time 4.87s (± 0.41%) 4.90s (± 0.35%) +0.03s (+ 0.62%) 4.87s 4.93s
Emit Time 2.73s (± 0.92%) 2.74s (± 0.65%) +0.01s (+ 0.33%) 2.69s 2.77s
Total Time 9.62s (± 0.48%) 9.67s (± 0.34%) +0.04s (+ 0.47%) 9.58s 9.73s
TFS - node (v16.17.1, x64)
Memory used 282,287k (± 0.01%) 282,271k (± 0.01%) -16k (- 0.01%) 282,134k 282,335k
Parse Time 1.17s (± 0.72%) 1.16s (± 0.86%) -0.01s (- 0.51%) 1.14s 1.18s
Bind Time 0.66s (± 3.70%) 0.66s (± 3.47%) +0.01s (+ 1.22%) 0.60s 0.69s
Check Time 4.75s (± 0.31%) 4.75s (± 0.57%) -0.00s (- 0.02%) 4.66s 4.79s
Emit Time 2.75s (± 1.87%) 2.75s (± 2.23%) -0.00s (- 0.07%) 2.65s 2.85s
Total Time 9.33s (± 0.74%) 9.32s (± 0.96%) -0.00s (- 0.04%) 9.12s 9.50s
material-ui - node (v16.17.1, x64)
Memory used 435,262k (± 0.01%) 435,295k (± 0.01%) +34k (+ 0.01%) 435,235k 435,489k
Parse Time 1.65s (± 0.42%) 1.65s (± 0.63%) +0.00s (+ 0.18%) 1.61s 1.66s
Bind Time 0.50s (± 0.80%) 0.50s (± 1.11%) +0.00s (+ 0.20%) 0.49s 0.52s
Check Time 11.87s (± 0.96%) 11.91s (± 0.63%) +0.04s (+ 0.30%) 11.78s 12.08s
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) 0.00s ( NaN%) 0.00s 0.00s
Total Time 14.02s (± 0.86%) 14.06s (± 0.54%) +0.04s (+ 0.28%) 13.92s 14.23s
xstate - node (v16.17.1, x64)
Memory used 515,970k (± 0.01%) 516,027k (± 0.01%) +57k (+ 0.01%) 515,959k 516,162k
Parse Time 2.30s (± 0.45%) 2.32s (± 0.37%) +0.02s (+ 0.87%) 2.31s 2.34s
Bind Time 0.84s (± 2.12%) 0.83s (± 0.91%) -0.01s (- 0.95%) 0.82s 0.85s
Check Time 1.35s (± 0.89%) 1.36s (± 0.49%) +0.01s (+ 0.52%) 1.34s 1.37s
Emit Time 0.06s (± 0.00%) 0.06s (± 0.00%) 0.00s ( 0.00%) 0.06s 0.06s
Total Time 4.56s (± 0.54%) 4.58s (± 0.28%) +0.02s (+ 0.46%) 4.54s 4.60s
System
Machine Namets-ci-ubuntu
Platformlinux 5.4.0-131-generic
Architecturex64
Available Memory16 GB
Available Memory15 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v16.17.1, x64)
Scenarios
  • Angular - node (v16.17.1, x64)
  • Compiler-Unions - node (v16.17.1, x64)
  • Monaco - node (v16.17.1, x64)
  • TFS - node (v16.17.1, x64)
  • material-ui - node (v16.17.1, x64)
  • xstate - node (v16.17.1, x64)
Benchmark Name Iterations
Current 51511 10
Baseline main 10

Developer Information:

Download Benchmark

@devanshj
Copy link
Author

devanshj commented Nov 16, 2022

Btw we can make it even more performant by bailing to default checking even more earlier by first checking for node.parent.kind === SyntaxKind.CallExpression. Currently there's an overhead while checking all object literals, with this change it'll be only for object literals which are a child of a call expression.

@devanshj
Copy link
Author

devanshj commented Nov 23, 2022

I was waiting for a green light as I said but I got bored :P so here are some advancements...

widen support for T extends <any-expression-that-has-T>

Previously only T extends F<T> was supported, now instead of just F<T> (ie a type reference which is a type alias with one argument), any type expression that has a reference to T in it's declaration is supported. Eg T extends { x: keyof T } was previously not supported but now with this commit it is.

support multiple passes

As I said in some cases two passes won't suffice. So we keep doing checks till we yield the same type as previous check, or we hit the limit of maximum 3 checks. If we hit the limit then we produce a error that says "Dependent contextual inference requires too many passes and possibly infinite". And perhaps this should be a warning instead of an error.

I've updated the todos in the description.

Comment on lines +28432 to +28433
((node as TypeReferenceNode).typeName as Identifier).escapedText ===
(contextualType.symbol.declarations![0] as TypeParameterDeclaration).name.escapedText
Copy link
Author

Choose a reason for hiding this comment

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

This is probably a brittle way of writing this (unless I'm mistaken) but I don't know any other way that works. I would have used symbols but it seems that they aren't declared at this point.

@@ -28417,6 +28417,96 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkObjectLiteral(node: ObjectLiteralExpression, checkMode?: CheckMode): Type {
const contextualType = getContextualType(node, /*contextFlags*/ undefined);
const isContextualTypeDependent =
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you tried using couldContainTypeVariables? A comment for this function mentions this:

    // Return true if the given type could possibly reference a type parameter for which
    // we perform type inference (i.e. a type parameter of a generic function). We cache
    // results for union and intersection types for performance reasons.

It sounds like maybe you could reuse it

Copy link
Author

Choose a reason for hiding this comment

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

No because here we don't want to check if X has any type parameter, we want to check if X has a T type parameter. So using isContextualTypeDependent = couldContainTypeVariable(contextualType.immediateConstraint) would make it true for even T extends F<U> (U being some other type parameter) when it should have been false (as it's not T extends F<T>).

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but then perhaps that function could be extended with an optional argument, and this way you could limit the results to be based on the specific type param.

Copy link
Author

@devanshj devanshj Dec 5, 2022

Choose a reason for hiding this comment

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

I thought about that but I think that's quite non-trivial because it's implemented keeping in mind that we're checking against any type parameter... So for example caching is a big part of this, right now because we're checking for any type parameter the cache is implemented simply as a CouldContainTypeVariables flag, but if we want to cache for couldContainTypeVariables(type, typeParameters) or even couldContainTypeVariable(type, typeParameter) it would require a lot of ceremony.

Edit: I think you could implement couldContainTypeVariable(type, typeParameter)'s cache by simply memoizing via ids of the arguments, so it's not much of a ceremony, but I'm not convinced I want to change this already existing abstraction for my use-case. Might reconsider in future.

Copy link
Author

@devanshj devanshj Jan 4, 2023

Choose a reason for hiding this comment

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

Btw I realised the implementation is seriouly true to it's name "couldContainTypeVariable" and not "containsTypeVariable". This function doesn't definitively checks if the type as a type parameter, it just kinda bets, for example if it's an anonymous object type it just bets it has a type parameter and doesn't check furthur inside it. So if it returns true that means the type either has a type parameter or doesn't have a type parameter, but if it return false then it the type definately doesn't have a type parameter. So one can write bailouts like if (!couldContainTypeVariable(type)) return can gain some optimization and still not give bad results.

So there's no way I can use this function here as I want a definitive result. And extending it's implementation to give a definitive result might not be trivial and definitately more complex and slow than the ast check I'm doing here.

Fun fact: While working on #52088 I overlooked the "could" in it's name knowing perfectly what it means and relied on it which produced bad results, finally when I looked at the implementation I saw that it really takes the "could" in it's name seriously :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
None yet
4 participants