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

Prefer by-val methods over in methods in overload resolution #23122

Merged
merged 8 commits into from
Dec 8, 2017
Merged

Prefer by-val methods over in methods in overload resolution #23122

merged 8 commits into from
Dec 8, 2017

Conversation

OmarTawfik
Copy link
Contributor

@OmarTawfik OmarTawfik commented Nov 10, 2017

Customer scenario

Given two methods with parameters that only differ by ref kind:

void M(int x) { }
void M(in int x) { }

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.

@OmarTawfik
Copy link
Contributor Author

OmarTawfik commented Nov 13, 2017

@dotnet-bot test this please #Closed

@OmarTawfik
Copy link
Contributor Author

OmarTawfik commented Nov 13, 2017

@dotnet/roslyn-compiler #Closed

@jcouv
Copy link
Member

jcouv commented Nov 13, 2017

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

@jaredpar
Copy link
Member

jaredpar commented Nov 13, 2017

@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

@jcouv jcouv requested a review from VSadov November 13, 2017 20:12
{
return BetterResult.Neither;
}
}

// 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)
Copy link
Member

@VSadov VSadov Nov 13, 2017

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

@OmarTawfik OmarTawfik Nov 14, 2017

Choose a reason for hiding this comment

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

@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

@jcouv jcouv changed the title Prefer by-val methods over in methdos in overload resolution Prefer by-val methods over in methods in overload resolution Nov 14, 2017
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);
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 15, 2017

Choose a reason for hiding this comment

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

ParameterSymbol parameter2 [](start = 109, length = 26)

Consider using discard. #Closed

{
return BetterResult.Neither;
}
}

// 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)
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 15, 2017

Choose a reason for hiding this comment

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

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

{
return BetterResult.Neither;
}
}

// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params,
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 15, 2017

Choose a reason for hiding this comment

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

// 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

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Nov 15, 2017

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

@OmarTawfik
Copy link
Contributor Author

OmarTawfik commented Nov 15, 2017

@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++)
{
Copy link
Member

@VSadov VSadov Nov 16, 2017

Choose a reason for hiding this comment

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

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

int m1ModifierCount = m1.LeastOverriddenMember.CustomModifierCount();
int m2ModifierCount = m2.LeastOverriddenMember.CustomModifierCount();
if (m1ModifierCount != m2ModifierCount)
{
return (m1ModifierCount < m2ModifierCount) ? BetterResult.Left : BetterResult.Right;
}

// Otherwise, prefer methods with 'val' parameters over 'in' parameters.
Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage");
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2017

Choose a reason for hiding this comment

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

"Unequal parameters counts are handled in an earlier stage" [](start = 65, length = 59)

Could you please point to the place that does that? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

I see that code comparing the number of parameters used, but not the number of parameters declared.


In reply to: 151544214 [](ancestors = 151544214,151523306)

int m1ModifierCount = m1.LeastOverriddenMember.CustomModifierCount();
int m2ModifierCount = m2.LeastOverriddenMember.CustomModifierCount();
if (m1ModifierCount != m2ModifierCount)
{
return (m1ModifierCount < m2ModifierCount) ? BetterResult.Left : BetterResult.Right;
}

// 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++)
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2017

Choose a reason for hiding this comment

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

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];
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2017

Choose a reason for hiding this comment

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

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;
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2017

Choose a reason for hiding this comment

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

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

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Nov 16, 2017

            return BetterResult.Neither;

Should we try to disambiguate here as well? #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/OverloadResolution.cs:1518 in d095bd7. [](commit_id = d095bd7, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Nov 16, 2017

Done with review pass (iteration 3) #Closed

@shyamnamboodiripad shyamnamboodiripad changed the base branch from post-dev15.5-contrib to master November 17, 2017 20:03
@OmarTawfik
Copy link
Contributor Author

OmarTawfik commented Nov 29, 2017

@VSadov @AlekseyTs comments addressed. #Closed

Copy link
Member

@VSadov VSadov left a comment

Choose a reason for hiding this comment

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

LGTM

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 5, 2017

    public override string ToString()

Should this method be adjusted? #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/Operators/UnaryOperatorSignature.cs:25 in c06fca6. [](commit_id = c06fca6, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 5, 2017

    public bool Equals(BinaryOperatorSignature other)

Should this method be adjusted? #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorSignature.cs:42 in c06fca6. [](commit_id = c06fca6, deletion_comment = False)

// Always prefer operators with val parameters over operators with in parameters:
BetterResult valOverInPreference;

if (op1.LeftRefKind == RefKind.None && op2.LeftRefKind == RefKind.In)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 5, 2017

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 5, 2017

Done with review pass (iteration 5). #Closed

@OmarTawfik
Copy link
Contributor Author

    public bool Equals(BinaryOperatorSignature other)

I'm assuming you mean ToString()?


In reply to: 349380594 [](ancestors = 349380594)


Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorSignature.cs:42 in c06fca6. [](commit_id = c06fca6, deletion_comment = False)

@OmarTawfik
Copy link
Contributor Author

OmarTawfik commented Dec 6, 2017

@AlekseyTs comments addressed. #Closed

@AlekseyTs
Copy link
Contributor

    public bool Equals(BinaryOperatorSignature other)

I'm assuming you mean ToString()?

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)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 6, 2017

Choose a reason for hiding this comment

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

Method [](start = 20, length = 6)

Please add cast to object. #Resolved

{
get
{
if (Method != null)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 6, 2017

Choose a reason for hiding this comment

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

Method [](start = 20, length = 6)

Please add cast to object. #Resolved

{
get
{
if (Method != null)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 6, 2017

Choose a reason for hiding this comment

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

Method [](start = 20, length = 6)

Please add cast to object. #Resolved

{
Debug.Assert(Method.ParameterRefKinds.Length == 1);

return Method.ParameterRefKinds.Single();
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 6, 2017

Choose a reason for hiding this comment

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

.Single() [](start = 55, length = 9)

Is Single() better than just [0]? #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will throw a more descriptive exception if something went wrong.


In reply to: 155394341 [](ancestors = 155394341)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (iteration 6)

@OmarTawfik
Copy link
Contributor Author

@MeiChin-Tsai for ask mode approval.

@jcouv
Copy link
Member

jcouv commented Dec 8, 2017

Ping for @MeiChin-Tsai for ask-mode approval (15.6). Thanks

@MeiChin-Tsai
Copy link

Approved.

@OmarTawfik
Copy link
Contributor Author

@dotnet-bot test windows_release_unit32_prtest

@OmarTawfik
Copy link
Contributor Author

@dotnet-bot test this please

@OmarTawfik OmarTawfik merged commit 919b606 into dotnet:master Dec 8, 2017
@OmarTawfik OmarTawfik deleted the val-in-tie-breaker branch December 8, 2017 22:56
@jcouv jcouv added this to the 15.6 milestone Dec 8, 2017
333fred added a commit to 333fred/roslyn that referenced this pull request Dec 9, 2017
* 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
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants