Skip to content

Tweak logic that chooses co- vs. contra-variant inferences when covariant inference is an empty object type - #59772

Closed
Mateusz Burzyński (Andarist) wants to merge 3 commits into
microsoft:mainfrom
Andarist:fix/co-contra-empty-co
Closed

Tweak logic that chooses co- vs. contra-variant inferences when covariant inference is an empty object type#59772
Mateusz Burzyński (Andarist) wants to merge 3 commits into
microsoft:mainfrom
Andarist:fix/co-contra-empty-co

Conversation

@Andarist

Copy link
Copy Markdown
Contributor

fixes#59765
cc Wesley Wigham (@weswigham) as the reviewer of #57909 by which this got affected

// and it would spoil the overall inference.
const preferCovariantType = inferredCovariantType && (!inferredContravariantType ||
!(inferredCovariantType.flags & (TypeFlags.Never | TypeFlags.Any)) &&
!(inferredCovariantType.flags & (TypeFlags.Never | TypeFlags.Any) || isEmptyAnonymousObjectType(inferredCovariantType) && isWeakType(inferredContravariantType)) &&

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is a conservative change that aims to address #59765 .

I could imagine this might need some further tweaking to produce good error locations when both of the inferred types are weak or something. However, so far so good. This already produces good results and good error locations as far as I can tell - perhaps part of that can be attributed to the further checks like some(inference.contraCandidates, t => isTypeAssignableTo(inferredCovariantType, t)).

@AndaristMateusz Burzyński (Andarist)Aug 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Actually, there is an open question here as to what the inferred type here should be (what should be the result's type):

declarefunctioncall<P>(tag: (props: P)=>void,attributes: P): P;declarefunctionconsume(p: {a?: string;b?: number;c?: boolean}): void;declareconstobj: {a?: string};constresult=call(consume,obj);

There is no clear answer to this - both satisfy the constraint and both of the arguments. I think that, in general, a covariant inference is safer in a situation like this. It's less likely to introduce problems like this one:

declarefunctioncall<P>(tag: (props: P)=>void,attributes: P): P;declarefunctionconsume(p: {a?: string;b?: number;c?: boolean}): void;constobj: {a?: string;}={a: 'foo',b: 'bar'}constresult=call(consume,obj);obj.b// it's `number` in 5.5 but at runtime it could easily be a `string`

Nowadays the user can use NoInfer to guide what should be used as the inference source if they don't get the desired inferences for their use case - it's all a little bit situational after all.

And from this perspective... this whole PR could just be closed as the current behavior (5.6-rc) isn't necessarily wrong. Duh, it could even be seen as desirable. All in all, this requires a ruling on what's the better default and all from the team.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I also experimented with an alternative patch:

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 954e45cdeb..f5e3d12778 100644
--- a/src/compiler/checker.ts+++ b/src/compiler/checker.ts@@ -26989,11 +26989,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Similarly ignore co-variant `any` inference when both are available as almost everything is assignable to it
// and it would spoil the overall inference.
const preferCovariantType = inferredCovariantType && (!inferredContravariantType ||
- !(inferredCovariantType.flags & (TypeFlags.Never | TypeFlags.Any) || isEmptyAnonymousObjectType(inferredCovariantType) && isWeakType(inferredContravariantType)) &&+ !(inferredCovariantType.flags & (TypeFlags.Never | TypeFlags.Any)) &&
some(inference.contraCandidates, t => isTypeAssignableTo(inferredCovariantType, t)) &&
every(context.inferences, other =>
other !== inference && getConstraintOfTypeParameter(other.typeParameter) !== inference.typeParameter ||
- every(other.candidates, t => isTypeAssignableTo(t, inferredCovariantType))));+ every(other.candidates, t => isTypeAssignableTo(t, inferredCovariantType))) &&+ (some(inference.candidates, t => !isObjectOrArrayLiteralType(t)) || isTypeAssignableTo(inferredCovariantType, inferredContravariantType) && !isTypeAssignableTo(inferredContravariantType, inferredCovariantType)));
inferredType = preferCovariantType ? inferredCovariantType : inferredContravariantType;
fallbackType = preferCovariantType ? inferredContravariantType : inferredCovariantType;
}

All tests pass with it with no changes whatsoever but then it makes those 2 to behave differently:

declarefunctioncall<P>(tag: (props: P)=>void,attributes: P): P;declarefunctionconsume(p: {a?: string;b?: number;c?: boolean}): void;declareconstobj1: {a?: string};constresult1=call(consume,obj1);constresult4=call(consume,{ ...obj1});

And all in all, I'm just not sure what's right here. It all boils down to choosing some tradeoffs.

@jakebailey

Copy link
Copy Markdown
Member

TypeScript Bot (@typescript-bot) test it

@typescript-bot

TypeScript Bot (typescript-bot) commented Aug 28, 2024

Copy link
Copy Markdown
Contributor

Starting jobs; this comment will be updated as builds start and complete.

CommandStatusResults
test top400✅ Started✅ Results
user test this✅ Started✅ Results
run dt✅ Started✅ Results
perf test this faster✅ Started👀 Results

@typescript-bot

Copy link
Copy Markdown
Contributor

Hey Jake Bailey (@jakebailey), the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey) Here are the results of running the user tests with tsc comparing main and refs/pull/59772/merge:

Everything looks good!

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey) Here are the results of running the top 400 repos with tsc comparing main and refs/pull/59772/merge:

Everything looks good!

@typescript-bot

Copy link
Copy Markdown
Contributor

Jake Bailey (@jakebailey)
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
MetricbaselineprDeltaBestWorstp-value
Compiler-Unions - node (v18.15.0, x64)
Errors3030~~~p=1.000 n=6
Symbols62,15362,153~~~p=1.000 n=6
Types50,24250,242~~~p=1.000 n=6
Memory used193,033k (± 0.73%)193,580k (± 0.93%)~192,398k195,914kp=0.298 n=6
Parse Time1.30s (± 0.64%)1.30s (± 0.80%)~1.29s1.31sp=0.181 n=6
Bind Time0.71s0.71s~~~p=1.000 n=6
Check Time9.58s (± 0.22%)9.59s (± 0.57%)~9.50s9.65sp=0.419 n=6
Emit Time2.72s (± 0.44%)2.71s (± 0.72%)~2.68s2.74sp=0.410 n=6
Total Time14.31s (± 0.16%)14.31s (± 0.37%)~14.22s14.38sp=0.935 n=6
angular-1 - node (v18.15.0, x64)
Errors77~~~p=1.000 n=6
Symbols945,753945,753~~~p=1.000 n=6
Types410,067410,067~~~p=1.000 n=6
Memory used1,222,695k (± 0.00%)1,222,709k (± 0.00%)~1,222,687k1,222,743kp=0.336 n=6
Parse Time6.64s (± 0.58%)6.65s (± 0.57%)~6.60s6.71sp=1.000 n=6
Bind Time1.86s1.86s (± 0.34%)~1.85s1.87sp=1.000 n=6
Check Time31.18s (± 0.33%)31.18s (± 0.62%)~30.90s31.42sp=0.936 n=6
Emit Time14.94s (± 0.47%)14.98s (± 0.66%)~14.79s15.07sp=0.090 n=6
Total Time54.63s (± 0.23%)54.67s (± 0.47%)~54.36s55.04sp=0.810 n=6
mui-docs - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols2,532,8412,532,841~~~p=1.000 n=6
Types996,405996,405~~~p=1.000 n=6
Memory used2,460,295k (± 0.00%)2,460,345k (± 0.00%)~2,460,255k2,460,459kp=0.378 n=6
Parse Time9.45s (± 0.20%)9.44s (± 0.22%)~9.41s9.46sp=0.935 n=6
Bind Time2.21s (± 0.74%)2.22s (± 0.47%)~2.20s2.23sp=1.000 n=6
Check Time75.08s (± 0.57%)75.17s (± 0.42%)~74.76s75.53sp=0.575 n=6
Emit Time0.29s (± 4.29%)0.28s~~~p=0.598 n=6
Total Time87.03s (± 0.50%)87.11s (± 0.39%)~86.65s87.48sp=0.575 n=6
self-build-src - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols1,232,1801,232,180~~~p=1.000 n=6
Types264,629264,629~~~p=1.000 n=6
Memory used2,532,437k (± 7.69%)2,532,360k (± 7.70%)~2,353,828k2,710,797kp=0.936 n=6
Parse Time6.05s (± 0.57%)6.05s (± 1.02%)~5.94s6.13sp=0.687 n=6
Bind Time2.26s (± 1.23%)2.25s (± 0.96%)~2.22s2.28sp=0.748 n=6
Check Time40.89s (± 0.69%)40.88s (± 0.97%)~40.45s41.45sp=0.810 n=6
Emit Time4.00s (± 0.69%)4.05s (± 1.19%)~4.00s4.12sp=0.128 n=6
Total Time53.24s (± 0.51%)53.25s (± 0.67%)~52.93s53.85sp=0.873 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols1,232,1801,232,180~~~p=1.000 n=6
Types264,629264,629~~~p=1.000 n=6
Memory used2,429,058k (± 0.03%)2,429,538k (± 0.04%)~2,428,360k2,431,221kp=0.298 n=6
Parse Time6.22s (± 0.30%)6.23s (± 0.87%)~6.13s6.28sp=0.687 n=6
Bind Time2.04s (± 0.81%)2.04s (± 0.91%)~2.02s2.07sp=1.000 n=6
Check Time41.59s (± 0.57%)41.42s (± 0.47%)~41.20s41.77sp=0.298 n=6
Emit Time4.05s (± 3.01%)4.08s (± 5.20%)~3.87s4.35sp=0.936 n=6
Total Time53.90s (± 0.43%)53.80s (± 0.50%)~53.52s54.11sp=0.689 n=6
self-compiler - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols257,016257,016~~~p=1.000 n=6
Types105,789105,789~~~p=1.000 n=6
Memory used429,701k (± 0.02%)429,752k (± 0.02%)~429,663k429,869kp=0.173 n=6
Parse Time4.16s (± 0.36%)4.17s (± 0.35%)~4.15s4.19sp=0.119 n=6
Bind Time1.60s (± 0.34%)1.60s (± 0.94%)~1.58s1.62sp=0.863 n=6
Check Time22.41s (± 0.23%)22.38s (± 0.29%)~22.26s22.46sp=0.687 n=6
Emit Time2.03s (± 1.18%)2.03s (± 0.80%)~2.01s2.05sp=1.000 n=6
Total Time30.20s (± 0.23%)30.19s (± 0.30%)~30.02s30.26sp=1.000 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors6868~~~p=1.000 n=6
Symbols225,018225,018~~~p=1.000 n=6
Types94,24994,249~~~p=1.000 n=6
Memory used370,206k (± 0.02%)370,214k (± 0.02%)~370,122k370,325kp=0.936 n=6
Parse Time2.29s (± 0.71%)2.29s (± 0.36%)~2.28s2.30sp=0.458 n=6
Bind Time1.33s (± 2.23%)1.32s (± 1.17%)~1.31s1.35sp=0.677 n=6
Check Time13.40s (± 0.41%)13.40s (± 0.46%)~13.29s13.46sp=0.936 n=6
Emit Time0.00s0.00s~~~p=1.000 n=6
Total Time17.03s (± 0.37%)17.00s (± 0.43%)~16.88s17.10sp=0.574 n=6
vscode - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols3,022,4153,022,415~~~p=1.000 n=6
Types1,039,9091,039,909~~~p=1.000 n=6
Memory used3,143,179k (± 0.00%)3,143,157k (± 0.00%)~3,143,070k3,143,304kp=0.810 n=6
Parse Time14.00s (± 0.57%)14.06s (± 0.82%)~13.94s14.27sp=0.378 n=6
Bind Time4.41s (± 2.89%)4.32s (± 2.24%)~4.27s4.52sp=0.075 n=6
Check Time80.36s (± 0.17%)80.47s (± 0.33%)~80.06s80.83sp=0.470 n=6
Emit Time20.62s (± 0.80%)20.60s (± 0.58%)~20.48s20.73sp=0.872 n=6
Total Time119.39s (± 0.18%)119.45s (± 0.33%)~118.76s119.83sp=0.521 n=6
webpack - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols275,318275,318~~~p=1.000 n=6
Types112,432112,432~~~p=1.000 n=6
Memory used424,236k (± 0.02%)424,199k (± 0.02%)~424,098k424,287kp=0.575 n=6
Parse Time3.98s (± 0.65%)3.98s (± 0.60%)~3.94s4.00sp=0.807 n=6
Bind Time1.73s (± 0.67%)1.72s (± 1.13%)~1.70s1.75sp=0.291 n=6
Check Time17.59s (± 0.30%)17.59s (± 0.63%)~17.37s17.66sp=0.292 n=6
Emit Time0.00s0.00s~~~p=1.000 n=6
Total Time23.29s (± 0.11%)23.29s (± 0.58%)~23.03s23.39sp=0.335 n=6
xstate-main - node (v18.15.0, x64)
Errors00~~~p=1.000 n=6
Symbols536,424536,424~~~p=1.000 n=6
Types177,440177,440~~~p=1.000 n=6
Memory used481,666k (± 0.10%)481,393k (± 0.05%)~481,222k481,920kp=0.809 n=6
Parse Time4.25s (± 0.71%)4.25s (± 0.89%)~4.21s4.31sp=0.809 n=6
Bind Time1.54s (± 1.06%)1.54s (± 0.53%)~1.53s1.55sp=0.616 n=6
Check Time22.52s (± 0.38%)22.50s (± 0.30%)~22.41s22.59sp=0.629 n=6
Emit Time0.00s0.00s~~~p=1.000 n=6
Total Time28.31s (± 0.33%)28.29s (± 0.36%)~28.17s28.46sp=0.689 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
BenchmarkNameIterations
Currentpr6
Baselinebaseline6

Developer Information:

Download Benchmarks

@RyanCavanaugh

Copy link
Copy Markdown
Member

I don't feel like this change is well-motivated enough to merge as-is. It seems like it's just detecting the repro in the linked issue and doing something else when it sees that, rather than a principled approach of what to do if all inference sites for a type parameter are intersections. Given the call on the linked issue it sounds like we're in agreement to not take this, but I'm definitely willing to have a discussion from first principles (just a log a new bug and we can take it from there).

@Andarist

Copy link
Copy Markdown
ContributorAuthor

I agree. I think that maybe a patch closer to this one could improve some scenarios but I'm also not sure. It's hard for me to know which tradeoffs should be preferred by the overall TS design in cases like this.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5.6 regression: Inference with mixed in type intersection no longer compiles

5 participants

@Andarist@jakebailey@typescript-bot@RyanCavanaugh@sandersn