You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Because our type system is structural, we often end up doing very deep comparisons of members.
Often we're able to "cheat" by seeing if the types are identical, or calculating variance and relating type arguments, but it's not always possible to avoid that.
More that just that, these structural checks can often go infinitely deep.
We used to say "if we see 5 levels of different instantiations of Box, we're not learning anything new" and stop.
"Turtle cutoff"
We've tweaked this over the years.
What does it mean to say "we saw 5 different instantiations of a type?"
Based on how a type was declared, we use some component of that which we call a "recursion identity". Whenever we relate types, we push these recursion identities on the stack. If you see the same identity some number of times in that stack, we stop.
We've had trouble with this in the past; have we fixed this?
Well this should have been fixed in the past.
A big part of this had to do with grabbing a recursion identity for the indexed access type (which is Foo for Foo[K1][K2][K3]...[Kn]).
One change in the PR is to swap the max depth to 3 for these checks.
This actually saves a ton of work in libraries whose types grow in a very "bushy" way.
Not a lot of tests here in the performance test suite though. 😔
But this exacerbates another issue which is - this cutoff is technically incorrect!
If you manually write outFoo<Foo<Foo<Foo<Foo<Foo<Foo<number>>>>>>> and Foo<Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>>, then you'll defeat the type-checker because of this depth limit!
Part of the issue is that a generated type is way more suspicious than something a user manually wrote out.
So the fix here: look at the type IDs!
If you have Foo<Foo<Foo<string>>>, each instantiation of Foo that's deeper in the type has a lower type ID
But if the type IDs always increase while relating, that's a sort of proof that the type system is generating new instantiations while relating.
So only count the type IDs that increase in the recursion stack.
Also - we have this logic that tries to leverage already-done checks around unconstrained type parameters.
When you have an instantiation of Foo and Bar with the same unconstrained type parameter, you've done an abstract comparison that applies to every single relation of Foo and Bar with the same type argument.
We do a non-trivial amount of work to look up whether that's been done and leverage that existing work.
The thing we used to do on that was running a regex on relationship cache keys.
We calculate these keys a little bit better now.
Ideas Around Performance in the Checker
We're thinking about performance a lot.
Canonicalize type parameter instantiations
Type parameters are a factory of new type IDs
map and filter and reduce etc. use a whole bunch of type parameters named U.
Each Collection<U> has a totally new instantiation.
From the checker's perspective, it's a waste of work; they're almost always identical.
Though from the language service's perspective, each U being distinct is meaningful for go-to-definition and find-all-refs.
What if we could canonicalize these types?
How much work is that canonicalization?
Easyish for unconstrainted type parameters.
More involved for constrained - have to "normalize" signatures first.
Determining references that can actually be narrowed
Enormous control flow graphs that need to be walked through for a variable just to find that it doesn't change anything.
70k calls to get the control-flow analyzed type, only 6800 actually change the type.
Binder could keep track of identifiers that occur in narrowing expressions in some qualifying scope (function body? source file?), and we could consult those before walking up the control flow graph.
Changes to Recursion Depth Checks
#46599
Because our type system is structural, we often end up doing very deep comparisons of members.
Often we're able to "cheat" by seeing if the types are identical, or calculating variance and relating type arguments, but it's not always possible to avoid that.
More that just that, these structural checks can often go infinitely deep.
We used to say "if we see 5 levels of different instantiations of
Box
, we're not learning anything new" and stop.What does it mean to say "we saw 5 different instantiations of a type?"
Foo
forFoo[K1][K2][K3]...[Kn]
).One change in the PR is to swap the max depth to 3 for these checks.
But this exacerbates another issue which is - this cutoff is technically incorrect!
Foo<Foo<Foo<Foo<Foo<Foo<Foo<number>>>>>>>
andFoo<Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>>
, then you'll defeat the type-checker because of this depth limit!So the fix here: look at the type IDs!
Foo<Foo<Foo<string>>>
, each instantiation ofFoo
that's deeper in the type has a lower type IDSo only count the type IDs that increase in the recursion stack.
Also - we have this logic that tries to leverage already-done checks around unconstrained type parameters.
Foo
andBar
with the same unconstrained type parameter, you've done an abstract comparison that applies to every single relation ofFoo
andBar
with the same type argument.Ideas Around Performance in the Checker
map
andfilter
andreduce
etc. use a whole bunch of type parameters namedU
.Collection<U>
has a totally new instantiation.U
being distinct is meaningful for go-to-definition and find-all-refs.getNodeId
less (forNodeLinks
)getNodeId
incompiler
outside of the checker #46595getNodeId
to get a valid Node IDMap
to aMap
WeakMap
- in theory faster than aMap
since theWeakMap
implementation could hold the value directly on theNode
.Node
type?WeakMap
s and their unpredictable performance.Map
vs.WeakMap
, you have to do a node ID-ectomy - remove all occurrences ofgetNodeId
.The text was updated successfully, but these errors were encountered: