Skip to content

Cache inference for recursive mapped types#20622

Closed
sandersn wants to merge 8 commits intomasterfrom
cache-inference-for-resursive-mapped-types
Closed

Cache inference for recursive mapped types#20622
sandersn wants to merge 8 commits intomasterfrom
cache-inference-for-resursive-mapped-types

Conversation

@sandersn
Copy link
Copy Markdown
Member

@sandersn sandersn commented Dec 11, 2017

Previously recursive mapped type inferences avoided cycles when looking for inferences. Now, they instead cache as they explore the object structure so that symbol-identical inferences to recursive mapped types don't repeat. This is needed, for example, to allow lodash's use of PartialDeep<T> to be usable with XMLHttpRequest and similarly large object types.

Also change to use the existing visited cache instead of a separate stack.

Edit: Also infer primitives directly to homomorphic mapped types. This improves nested inferences a lot.

Previously recursive mapped type inferences avoided cycles when looking
for inferences. Now, they instead cache as they explore the object
structure so that symbol-identical inferences to recursive mapped types
don't repeat. This is needed, for example, to allow lodash's use of
PartialDeep<T> to be usable with XMLHttpRequest and similarly large
object types.

Also change to use the existing visited cache instead of a separate
stack.
Compilation didn't finish before! Now it does.
@sandersn sandersn requested a review from weswigham December 11, 2017 17:51
@sandersn
Copy link
Copy Markdown
Member Author

Improves the fix in #20370. Turns out the test there was too small for real types like XMLHttpRequest.

Copy link
Copy Markdown
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

Sans the cache key being a bit odd (maybe a comment explaining that the cache can contain lists of symbols and types would be in order), this looks pretty straightforward.

Oh! One more thing: The type baseline has inferences like readonly readyState: { toString: {}; toFixed: {}; toExponential: {}; toPrecision: {}; valueOf: {}; toLocaleString: {}; };. I know you mentioned skipping primitives before, but preventing an inference like this is probably a good reason to actually do it. If I recall, we also don't actually apply mapped types to primitives Partial<number> is just number, so inferring a mapped type to a primitive (even structurally) seems like it's always incorrect. Maybe deserves another issue.

Comment thread src/compiler/checker.ts Outdated
if (inference && !inference.isFixed) {
const key = (source.symbol ? getSymbolId(source.symbol) + "," : "") + getSymbolId(target.symbol);
if (contains(mappedTypeStack, key)) {
const key = (source.symbol ? getSymbolId(source.symbol) + "s" : "") + getSymbolId(target.symbol);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Normally we distinguish different types of cache keys with a leading character, rather than a different separator, see getLiteralType for prior art. Plus, I think something like T123,12 vs S11,14 is going to make more sense in the debugger than 123,12 vs 11s14.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sure. Done.

@weswigham
Copy link
Copy Markdown
Member

If I recall, we also don't actually apply mapped types to primitives Partial<number> is justnumber, so inferring a mapped type to a primitive (even structurally) seems like it's always incorrect. Maybe deserves another issue.

Or rather, it's like that when inferring from MappedType<T> to a primitive, T should be inferred as the primitive, to mimic our actual mapped type application behavior.

@sandersn
Copy link
Copy Markdown
Member Author

It's easy enough to skip primitives. I'll poke at inferring them too, and include it if it's simple enough.

@sandersn
Copy link
Copy Markdown
Member Author

OK, I infer homomorphic mapped types to primitives. The change is simple but I'm not 100% sure it's correct. @ahejlsberg can you take a look?

@sandersn sandersn requested a review from ahejlsberg December 11, 2017 19:05
@weswigham
Copy link
Copy Markdown
Member

I still see readonly SVG_MEETORSLICE_SLICE: {} in the output, but readonly SVG_MEETORSLICE_SLICE is a member of SVGPreserveAspectRatio of type number. What's up with that?

Comment thread src/compiler/checker.ts
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, mappedTypeStack: string[]): Type {
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, visited: Map<Type | undefined>): Type {
if (source.flags & TypeFlags.Primitive) {
return source;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this tested anywhere in a more obvious way? e.g.

// @declaration: true

type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

export declare function foo<T>(x: MyReadonly<T>): T;

export let x = foo(100);

The original inference cache lookup used Map.get instead of Map.has.
With that fix, I had to split the caches back into two so that the
original type-only lookup restarted for each call inside
inferTypeForHomomorphicMappedTypes.
@mhegazy
Copy link
Copy Markdown
Contributor

mhegazy commented Jan 9, 2018

closing in favor of #20711

@mhegazy mhegazy closed this Jan 9, 2018
@sandersn sandersn deleted the cache-inference-for-resursive-mapped-types branch January 9, 2018 16:43
@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
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.

4 participants