Prefer by-val methods over in methods in overload resolution#23122
Prefer by-val methods over in methods in overload resolution#23122OmarTawfik merged 8 commits intodotnet:masterfrom OmarTawfik:val-in-tie-breaker
Conversation
|
@dotnet-bot test this please #Closed |
|
@dotnet/roslyn-compiler #Closed |
|
I'm wondering if we should move this to C# 7.3 and gate by language version. We don't want different people on a team seeing two different behaviors for this. #Resolved |
|
@jcouv every release of the compiler changes overload resolution: some more explicitly than others. So far we've avoided using the language version to toggle how overload resolution works. Mostly because it adds layers to the complexity matrix of an already incredibly hard problem. #Resolved |
|
|
||
| // If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one. | ||
| // Except if it was params. Subsequent betterness analysis will always prefer the non-params one. | ||
| if (!p1.IsParams && p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) |
There was a problem hiding this comment.
This is confusing.
If we have
M1(params int[] arr) . . . and
M1(in int[] arr) ...And I call with M1( new int[] {1,2}) - i.e. first is applicable in normal (not expanded) form
I would expect the byval method to be selected, regardless if it was declared params or not. The comment seems to say the opposite.
We need some tests for this scenario - with params overload matching in normal or expanded form. (in expanded form I expect it to not be selected) #Resolved
There was a problem hiding this comment.
@VSadov We already have a test that tests params expansion with in: PassingInArgumentsOverloadedOnInParams.
For the array variation, we are still reporting ambiguity (I prefer this over selecting the by-val one, as it can break the caller). Will add another test for that variation and explain in the comment. #Closed
| var type1 = GetParameterType(i, m1.Result, m1.LeastOverriddenMember.GetParameters(), out refKind1); | ||
| var type2 = GetParameterType(i, m2.Result, m2.LeastOverriddenMember.GetParameters(), out refKind2); | ||
| var type1 = GetParameterType(i, m1.Result, m1.LeastOverriddenMember.GetParameters(), out ParameterSymbol parameter1); | ||
| var type2 = GetParameterType(i, m2.Result, m2.LeastOverriddenMember.GetParameters(), out ParameterSymbol parameter2); |
There was a problem hiding this comment.
ParameterSymbol parameter2 [](start = 109, length = 26)
Consider using discard. #Closed
|
|
||
| // If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, | ||
| // for later betterness analysis will decide on whether the expanded/non-expanded form can be used, or if it is ambiguous. | ||
| if (!p1.IsParams && p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) |
There was a problem hiding this comment.
p1.IsParams [](start = 17, length = 11)
See IsValidParams helper how to check for valid params parameter. Also, why should this matter if candidate is not using expanded form? #Closed
| } | ||
| } | ||
|
|
||
| // If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, |
There was a problem hiding this comment.
// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, [](start = 12, length = 112)
It feels suspicious that we do this check before we checked which candidate uses BetterConversionFromExpression. Even if we do that immediately after, we might still introduce a breaking change as the tie breaking rules that are executed today after this function can resolve the ambiguity differently. #Closed
|
Done with review pass (iteration 2). It feels like we need to figure out exact rules and where do they fit in the current overload resolution specification. Then we can review the change. #Closed |
|
@AlekseyTs I addressed the comments. Will follow up on the spec change in the mean time. #Closed |
| // Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
| Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); | ||
| for (i = 0; i < m1Original.Length; i++) | ||
| { |
There was a problem hiding this comment.
It seems the order of paremeters matters.
I.E. we can resolve M(1, 1) when
M(in int x, int y)
M(int x, in int y)
Second wins. Is that intentional? #Resolved
| } | ||
|
|
||
| // Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
| Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); |
There was a problem hiding this comment.
"Unequal parameters counts are handled in an earlier stage" [](start = 65, length = 59)
Could you please point to the place that does that? #Closed
There was a problem hiding this comment.
Check lines 1441-1519. They should handle these cases where we end up with mismatching parameter count because of unused optional or expanded ones. I'll add a test as well.
In reply to: 151523306 [](ancestors = 151523306)
There was a problem hiding this comment.
I see that code comparing the number of parameters used, but not the number of parameters declared.
In reply to: 151544214 [](ancestors = 151544214,151523306)
|
|
||
| // Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
| Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); | ||
| for (i = 0; i < m1Original.Length; i++) |
There was a problem hiding this comment.
for (i = 0; i < m1Original.Length; i++) [](start = 12, length = 39)
I think we should only look at parameters that have arguments. #Closed
| for (i = 0; i < m1Original.Length; i++) | ||
| { | ||
| var p1 = m1Original[i]; | ||
| var p2 = m2Original[i]; |
There was a problem hiding this comment.
var p2 = m2Original[i]; [](start = 16, length = 23)
It looks like these parameters may correspond to different arguments. Why would we look at them in this case? #Closed
|
|
||
| if (p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) | ||
| { | ||
| return BetterResult.Left; |
There was a problem hiding this comment.
return BetterResult.Left; [](start = 20, length = 25)
Returning too early? Please add tests for multiple arguments including cases when different arguments point to a different winners. #Closed
|
Done with review pass (iteration 3) #Closed |
|
@VSadov @AlekseyTs comments addressed. #Closed |
|
@OmarTawfik It looks like there are legitimate test failures #Closed |
| get | ||
| { | ||
| Debug.Assert( | ||
| Method != null && |
There was a problem hiding this comment.
Method != null && [](start = 20, length = 17)
Same comment as for Unary operator. #Closed
| get | ||
| { | ||
| Debug.Assert( | ||
| Method != null && |
There was a problem hiding this comment.
Method != null && [](start = 20, length = 17)
Same comment. Consider passing ref-kinds to the constructor explicitly, the same way as we do with types. #Closed
There was a problem hiding this comment.
As an alternative, we could always return None when Method is null.
In reply to: 155020652 [](ancestors = 155020652)
There was a problem hiding this comment.
Will take into account null methods (built-in language operators that have no method symbol).
In reply to: 155021847 [](ancestors = 155021847,155020652)
| // Always prefer operators with val parameters over operators with in parameters: | ||
| BetterResult valOverInPreference; | ||
|
|
||
| if (op1.LeftRefKind == RefKind.None && op2.LeftRefKind == RefKind.In) |
There was a problem hiding this comment.
if (op1.LeftRefKind == RefKind.None && op2.LeftRefKind == RefKind.In) [](start = 12, length = 69)
It feels like the logic here could be simplified if we add a helper that calculates the best among two ref kinds. Then we call it for both sides and then reason about final answer.
Something like this:
BetterResult fromLeft = better(op1.LeftRefKind, op2.LeftRefKind);
BetterResult fromRight = better(op1.RightRefKind, op2.RightRefKind);
if (fromLeft == BetterResult.Neither || fromLeft == fromRight)
return fromRight;
if (fromRight == BetterResult.Neither)
return fromLeft;
return BetterResult.Neither;
``` #Resolved
There was a problem hiding this comment.
To me, the existing implementation is easier to read/trace. However, I don't feel strongly about this. I can implement your suggestions if you think it is the better alternative.
In reply to: 155026276 [](ancestors = 155026276)
|
Done with review pass (iteration 5). #Closed |
|
@AlekseyTs comments addressed. #Closed |
I meant "Equals" implementation, but, given how LeftRefKind/RightRefKind are implemented, the current implementation should be fine. In reply to: 349467602 [](ancestors = 349467602,349380594) Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorSignature.cs:42 in c06fca6. [](commit_id = c06fca6, deletion_comment = False) |
| { | ||
| get | ||
| { | ||
| if (Method != null) |
There was a problem hiding this comment.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
| { | ||
| get | ||
| { | ||
| if (Method != null) |
There was a problem hiding this comment.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
| { | ||
| get | ||
| { | ||
| if (Method != null) |
There was a problem hiding this comment.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
| { | ||
| Debug.Assert(Method.ParameterRefKinds.Length == 1); | ||
|
|
||
| return Method.ParameterRefKinds.Single(); |
There was a problem hiding this comment.
.Single() [](start = 55, length = 9)
Is Single() better than just [0]? #Resolved
There was a problem hiding this comment.
It will throw a more descriptive exception if something went wrong.
In reply to: 155394341 [](ancestors = 155394341)
|
@MeiChin-Tsai for ask mode approval. |
|
Ping for @MeiChin-Tsai for ask-mode approval (15.6). Thanks |
|
Approved. |
|
@dotnet-bot test windows_release_unit32_prtest |
|
@dotnet-bot test this please |
* dotnet/master: (39 commits) Prefer by-val methods over in methods in overload resolution (dotnet#23122) Update build to account for additional Mono.Cecil assemblies Fix build break Enable multicore JIT in the compilers (dotnet#23173) Separate debugging workspace service and EnC service (dotnet#23630) UseInferredMemberName: use one code style option and share more code (dotnet#23506) Add negative test cases Remove unnecessary Mono.Cecil reference Updated formatting of decompiler license notice Use sentence case for the decompiler legal notice Use ConcurrentDictionary to avoid locks in IsGeneratedCode and HasHiddenRegions Recognize condition with logical negation Refactor so that GetSymbolInfo can be called last Change the code fix to find the expression so that we don't need to unwrap the argument Add check for null argument syntax Fix argument names Formatting adjustments Add VB tests for omitted arguments Add WorkItem attributes Pool the Stopwatch instance used in the analyzer driver ...
Customer scenario
Given two methods with parameters that only differ by ref kind:
The call
M(int)would result in (C# 7.2, shipped with VS 15.5):error CS0121: The call is ambiguous between the following methods or properties: 'C.M(int)' and 'C.M(in int)'This PR proposes to change that behavior, and always prefer value methods over in methods. This is not a breaking change, because it should only affect cases where the compiler produced an error, to turn it into valid invocations.
[Note from jcouv:] This change would ship in 15.6 preview 2 as a bug fix for C# 7.2 (without introducing a new language version).
Bugs this fixes
Contributes to dotnet/csharplang#945 (comment)
Workarounds, if any
None needed
Risk
Low. This should be a new tie-breaker rule that is introduced to C# overload resolution.
Performance impact
None.
Is this a regression from a previous update?
No. This is a new feature.
Test documentation updated?
No.