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

Method group natural type: only use methods that can be fully substituted and pass constraint checks #69226

Merged
merged 5 commits into from
Nov 16, 2023

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Jul 26, 2023

Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/method-group-natural-type-improvements.md

There's three parts to this change:

  1. we check whether generic extension methods can be fully substituted instead of checking for arity
  2. we check for type constraints
  3. both of those checks occur early (which allows pruning candidates within a given scope)

Relates to test plan #69432

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Jul 26, 2023
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jul 26, 2023
@jcouv jcouv self-assigned this Jul 26, 2023
@jcouv jcouv added this to the 17.8 milestone Jul 26, 2023
@jcouv jcouv force-pushed the unique-sig2 branch 6 times, most recently from dc31635 to b2afd51 Compare July 27, 2023 21:58
@jcouv jcouv modified the milestones: 17.8, 17.9 Aug 1, 2023
@jcouv jcouv force-pushed the unique-sig2 branch 2 times, most recently from 3d83756 to 4ed1916 Compare November 3, 2023 19:38
@jcouv jcouv removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Nov 3, 2023
@jcouv jcouv marked this pull request as ready for review November 6, 2023 19:13
@jcouv jcouv requested a review from a team as a code owner November 6, 2023 19:13
@jcouv jcouv requested review from AlekseyTs and cston November 6, 2023 19:13
@@ -1231,9 +1264,7 @@ partial class B : A
yield return getData("internal static void F(this object x, ref object y) { }", "internal static void F(this object x, object y) { }", "this.F", "F"); // different parameter ref kind
yield return getData("internal static object F(this object x) => throw null;", "internal static ref object F(this object x) => throw null;", "this.F", "F"); // different return ref kind
yield return getData("internal static ref object F(this object x) => throw null;", "internal static object F(this object x) => throw null;", "this.F", "F"); // different return ref kind
yield return getData("internal static void F(this object x, object y) { }", "internal static void F<T>(this object x, T y) { }", "this.F", "F"); // different arity
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

yield return getData("internal static void F(this object x, object y) { }", "internal static void F(this object x, T y) { }", "this.F", "F");

Instead of removing this entry, consider changing the expected result, by adding the following arguments: ..., null, "A.F", "System.Action<System.Object>"); #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

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

Although the added tests are unfortunately verbose, I find them much more readable

yield return getData("internal static void F(this object x, object y) { }", "internal static void F<T>(this object x, T y) { }", "this.F<int>", "F<int>", null, "B.F", "System.Action<System.Int32>"); // different arity
yield return getData("internal static void F<T>(this object x) { }", "internal static void F(this object x) { }", "this.F", "F"); // different arity
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

yield return getData("internal static void F(this object x) { }", "internal static void F(this object x) { }", "this.F", "F");

Instead of removing this entry, consider changing the expected result, by adding the following arguments: ..., null, "B.F", "System.Action"); #ByDesign

// (5,34): error CS0123: No overload for 'F' matches delegate 'Action'
// System.Delegate d = this.F<int>;
Diagnostic(ErrorCode.ERR_MethDelegateMismatch, "F<int>").WithArguments("F", "System.Action").WithLocation(5, 34)
}); // different type parameter constraints
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

Instead of removing this entry, consider changing the expected result, by removing the diagnostic argument. #ByDesign

// (6,34): error CS0123: No overload for 'F' matches delegate 'Action'
// System.Delegate d = this.F<int>;
Diagnostic(ErrorCode.ERR_MethDelegateMismatch, "F<int>").WithArguments("F", "System.Action").WithLocation(6, 34)
}); // different type parameter constraints
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

Consider changing the expected result, by removing the diagnostic argument. #ByDesign

[Fact]
public void GenericExtensionMethod_Constraint()
{
// In C# 12, a method group that cannot be successfully substituted (ie. respecting constraints)
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

C# 12

C#13? #Resolved

[Fact]
public void GenericInstanceMethod_Constraint()
{
// In C# 12, a method that cannot be successfully substituted (ie. respecting constraints)
Copy link
Member

@cston cston Nov 7, 2023

Choose a reason for hiding this comment

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

C# 12

C#13? #Resolved

{
return null;
}
foreach (var instanceMethod in node.Methods)
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

instanceMethod

The name feels somewhat confusing. The method can be a static method, right? "memberMethod" instead? #Closed

return null;
}

// For the purpose of construction we use original type parameters in place of type arguments that we couldn't infer from the first argument.
ImmutableArray<TypeWithAnnotations> typeArgsForConstruct = typeArgs;
if (typeArgs.Any(static t => !t.HasType))
wasFullyInferred = !typeArgs.Any(static t => !t.HasType);
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

!typeArgs.Any(static t => !t.HasType)

typeArgs.All(static t => t.HasType)? #Closed

""";
var comp = CreateCompilation(new[] { source, s_utils }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe);
CompileAndVerify(comp, expectedOutput: "System.Action");
comp.VerifyDiagnostics();
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

.VerifyDiagnostics()

Consider verifying diagnostics on result of CompileAndVerify #Closed

public void M()
{
System.Delegate d = F;
System.Console.Write(d.GetDelegateTypeName());
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

d.GetDelegateTypeName()

I think it will be useful to invoke the delegate and observe which method is invoked. #Closed

}
""";
var comp = CreateCompilation(new[] { source, s_utils }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe);
comp.VerifyDiagnostics();
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

.VerifyDiagnostics();

Consider verifying diagnostics on result of CompileAndVerify instead. Applicable to other added tests as well. #Closed

@@ -1838,15 +1984,56 @@ static class B
internal static void F(this object x) { }
}
}
""";
var comp = CreateCompilation(new[] { source, s_utils }, parseOptions: useCSharp13 ? TestOptions.RegularNext : TestOptions.RegularPreview);
comp.VerifyDiagnostics();
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

comp.VerifyDiagnostics();

It looks like this becomes a success scenario. We should verify it as such, i.e. execute and observe the behavior #Closed

// var z = new C().M;
Diagnostic(ErrorCode.ERR_CannotInferDelegateType, "new C().M").WithLocation(4, 9)
);
comp.VerifyDiagnostics();
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

comp.VerifyDiagnostics();

Verify as success scenario? Applicable to other modified tests as well #Closed

// var x = new object().F;
Diagnostic(ErrorCode.ERR_CannotInferDelegateType, "new object().F").WithLocation(1, 9));

CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

RegularNext

What about the latest version? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is applicable to other tests as well.

[Fact]
public void GenericExtensionMethod_ArityCountsInSignature()
{
// Arity counts as part of "signature" so we have two different signatures even after reducing extension methods
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

// Arity counts as part of "signature" so we have two different signatures even after reducing extension methods

This doesn't feel right. Since all type arguments are supplied, they shouldn't matter. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that makes sense

internal static void F(this object x) { }
}
""";
CreateCompilation(source, parseOptions: TestOptions.Regular11).VerifyDiagnostics(
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 8, 2023

Choose a reason for hiding this comment

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

Regular11

Regular12? It looks like this is applicable to other tests as well. #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 1)

@@ -338,6 +338,7 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
considerTypeConstraints: false,
refKindCompareMode: RefKindCompareMode.ConsiderDifferences,
considerCallingConvention: false,
considerArity: false,
Copy link
Member Author

@jcouv jcouv Nov 14, 2023

Choose a reason for hiding this comment

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

📝 although the legacy logic uses this comparer, it is not affected by this change since it only performs substitution/reduction once a unique signature is found. #Closed

@jcouv jcouv requested a review from AlekseyTs November 14, 2023 21:48
@@ -338,6 +338,7 @@ internal sealed class MemberSignatureComparer : IEqualityComparer<Symbol>
considerTypeConstraints: false,
refKindCompareMode: RefKindCompareMode.ConsiderDifferences,
considerCallingConvention: false,
considerArity: false,
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 15, 2023

Choose a reason for hiding this comment

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

false

although the legacy logic uses this comparer, it is not affected by this change since it only performs substitution/reduction once a unique signature is found.

Let's suppose we have two generic methods with different arity, but with the same signature. For example, none of them refer to their type parameters in the signature. Isn't this change going to affect their comparison before substitution?
It feels to me, that keeping a separate comparer for the legacy logic is a small enough price to pay for a peace of mind that the logic isn't broken. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right, the difference is visible in that scenario. Added a test and a legacy comparer. Thanks!

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 4)

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 (commit 5)

@jcouv jcouv enabled auto-merge (squash) November 16, 2023 01:17
@jcouv jcouv merged commit f82627c into dotnet:main Nov 16, 2023
25 checks passed
@ghost ghost modified the milestones: 17.9, Next Nov 16, 2023
@RikkiGibson RikkiGibson modified the milestones: Next, 17.9 P2 Nov 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants