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
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 101 additions & 64 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9869,98 +9869,135 @@ static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
return GetUniqueSignatureFromMethodGroup_CSharp10(node);
}

MethodSymbol? method = selectMethodForSignature(node);

if (method is null)
MethodSymbol? foundMethod = null;
var typeArguments = node.TypeArgumentsOpt;
if (node.ResultKind == LookupResultKind.Viable)
{
return null;
}
foreach (var memberMethod in node.Methods)
{
switch (node.ReceiverOpt)
{
case BoundTypeExpression:
case null: // if `using static Class` is in effect, the receiver is missing
if (!memberMethod.IsStatic) continue;
break;
case BoundThisReference { WasCompilerGenerated: true }:
break;
default:
if (memberMethod.IsStatic) continue;
break;
}

int arity = node.TypeArgumentsOpt.IsDefaultOrEmpty ? 0 : node.TypeArgumentsOpt.Length;
if (method.Arity != arity)
{
return null;
}
else if (arity > 0)
{
return method.ConstructedFrom.Construct(node.TypeArgumentsOpt);
}
int arity = typeArguments.IsDefaultOrEmpty ? 0 : typeArguments.Length;
if (memberMethod.Arity != arity)
{
// We have no way of inferring type arguments, so if the given type arguments
// don't match the method's arity, the method is not a candidate
continue;
}

return method;
var substituted = typeArguments.IsDefaultOrEmpty ? memberMethod : memberMethod.Construct(typeArguments);
if (!satisfiesConstraintChecks(substituted))
{
continue;
}

static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
{
if (method is null)
{
method = candidate;
return true;
if (!isCandidateUnique(ref foundMethod, substituted))
{
return null;
}
}
if (MemberSignatureComparer.MethodGroupSignatureComparer.Equals(method, candidate))

if (foundMethod is not null)
{
return true;
return foundMethod;
}
method = null;
return false;
}

MethodSymbol? selectMethodForSignature(BoundMethodGroup node)
if (node.SearchExtensionMethods)
{
MethodSymbol? method = null;
if (node.ResultKind == LookupResultKind.Viable)
var receiver = node.ReceiverOpt!;
var methodGroup = MethodGroup.GetInstance();
foreach (var scope in new ExtensionMethodScopes(this))
{
foreach (var m in node.Methods)
methodGroup.Clear();
PopulateExtensionMethodsFromSingleBinder(scope, methodGroup, node.Syntax, receiver, node.Name, typeArguments, BindingDiagnosticBag.Discarded);
foreach (var extensionMethod in methodGroup.Methods)
{
switch (node.ReceiverOpt)
var substituted = typeArguments.IsDefaultOrEmpty ? extensionMethod : extensionMethod.Construct(typeArguments);

var reduced = substituted.ReduceExtensionMethod(receiver.Type, Compilation, out bool wasFullyInferred);
if (reduced is null)
{
case BoundTypeExpression:
case null: // if `using static Class` is in effect, the receiver is missing
if (!m.IsStatic) continue;
break;
case BoundThisReference { WasCompilerGenerated: true }:
break;
default:
if (m.IsStatic) continue;
break;
// Extension method was not applicable
continue;
}

if (!isCandidateUnique(ref method, m))
if (!wasFullyInferred)
{
continue;
}

if (!satisfiesConstraintChecks(reduced))
{
continue;
}

var wasUnique = isCandidateUnique(ref foundMethod, reduced);
if (!wasUnique)
{
methodGroup.Free();
return null;
}
}

if (method is not null)
if (foundMethod is not null)
{
return method;
methodGroup.Free();
return foundMethod;
}
}
methodGroup.Free();
}

if (node.SearchExtensionMethods)
return null;

static bool isCandidateUnique(ref MethodSymbol? foundMethod, MethodSymbol candidate)
{
if (foundMethod is null)
{
var receiver = node.ReceiverOpt!;
foreach (var scope in new ExtensionMethodScopes(this))
{
var methodGroup = MethodGroup.GetInstance();
PopulateExtensionMethodsFromSingleBinder(scope, methodGroup, node.Syntax, receiver, node.Name, node.TypeArgumentsOpt, BindingDiagnosticBag.Discarded);
foreach (var m in methodGroup.Methods)
{
if (m.ReduceExtensionMethod(receiver.Type, Compilation) is { } reduced &&
!isCandidateUnique(ref method, reduced))
{
methodGroup.Free();
return null;
}
}
methodGroup.Free();
foundMethod = candidate;
return true;
}
if (MemberSignatureComparer.MethodGroupSignatureComparer.Equals(foundMethod, candidate))
{
return true;
}
foundMethod = null;
return false;
}

if (method is not null)
{
return method;
}
}
bool satisfiesConstraintChecks(MethodSymbol method)
{
if (method.Arity == 0)
{
return true;
}

return null;
var diagnosticsBuilder = ArrayBuilder<TypeParameterDiagnosticInfo>.GetInstance();
ArrayBuilder<TypeParameterDiagnosticInfo>? useSiteDiagnosticsBuilder = null;

bool constraintsSatisfied = ConstraintsHelper.CheckMethodConstraints(
method,
new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, location: NoLocation.Singleton, diagnostics: null),
diagnosticsBuilder,
nullabilityDiagnosticsBuilderOpt: null,
ref useSiteDiagnosticsBuilder);

diagnosticsBuilder.Free();
useSiteDiagnosticsBuilder?.Free();

return constraintsSatisfied;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ private void InterceptCallAndAdjustArguments(
// we need to take special care to intercept with the extension method as though it is being called in reduced form.
Debug.Assert(receiverOpt is not BoundTypeExpression || method.IsStatic);
var needToReduce = receiverOpt is not (null or BoundTypeExpression) && interceptor.IsExtensionMethod;
var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation) : interceptor;
var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation, out _) : interceptor;

if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

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!

typeComparison: TypeCompareKind.AllIgnoreOptions);

// Compare the "unqualified" part of the member name (no explicit part)
Expand Down
10 changes: 8 additions & 2 deletions src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,14 +733,19 @@ public override TResult Accept<TResult>(CSharpSymbolVisitor<TResult> visitor)
return visitor.VisitMethod(this);
}

public MethodSymbol ReduceExtensionMethod(TypeSymbol receiverType, CSharpCompilation compilation)
{
return ReduceExtensionMethod(receiverType, compilation, wasFullyInferred: out _);
}

/// <summary>
/// If this is an extension method that can be applied to a receiver of the given type,
/// returns a reduced extension method symbol thus formed. Otherwise, returns null.
/// </summary>
/// <param name="compilation">The compilation in which constraints should be checked.
/// Should not be null, but if it is null we treat constraints as we would in the latest
/// language version.</param>
public MethodSymbol ReduceExtensionMethod(TypeSymbol receiverType, CSharpCompilation compilation)
public MethodSymbol ReduceExtensionMethod(TypeSymbol receiverType, CSharpCompilation compilation, out bool wasFullyInferred)
{
if ((object)receiverType == null)
{
Expand All @@ -749,10 +754,11 @@ public MethodSymbol ReduceExtensionMethod(TypeSymbol receiverType, CSharpCompila

if (!this.IsExtensionMethod || this.MethodKind == MethodKind.ReducedExtension || receiverType.IsVoidType())
{
wasFullyInferred = false;
return null;
}

return ReducedExtensionMethodSymbol.Create(this, receiverType, compilation);
return ReducedExtensionMethodSymbol.Create(this, receiverType, compilation, out wasFullyInferred);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ internal sealed class ReducedExtensionMethodSymbol : MethodSymbol
/// </summary>
/// <param name="compilation">Compilation used to check constraints.
/// The latest language version is assumed if this is null.</param>
public static MethodSymbol Create(MethodSymbol method, TypeSymbol receiverType, CSharpCompilation compilation)
public static MethodSymbol Create(MethodSymbol method, TypeSymbol receiverType, CSharpCompilation compilation, out bool wasFullyInferred)
{
Debug.Assert(method.IsExtensionMethod && method.MethodKind != MethodKind.ReducedExtension);
Debug.Assert(method.ParameterCount > 0);
Debug.Assert((object)receiverType != null);

var useSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.DiscardedDependencies;

method = InferExtensionMethodTypeArguments(method, receiverType, compilation, ref useSiteInfo);
method = InferExtensionMethodTypeArguments(method, receiverType, compilation, ref useSiteInfo, out wasFullyInferred);
if ((object)method == null)
{
return null;
Expand Down Expand Up @@ -109,19 +109,22 @@ private ReducedExtensionMethodSymbol(MethodSymbol reducedFrom)
/// are not satisfied, the return value is null.
/// </summary>
/// <param name="compilation">Compilation used to check constraints. The latest language version is assumed if this is null.</param>
private static MethodSymbol InferExtensionMethodTypeArguments(MethodSymbol method, TypeSymbol thisType, CSharpCompilation compilation, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
private static MethodSymbol InferExtensionMethodTypeArguments(MethodSymbol method, TypeSymbol thisType, CSharpCompilation compilation,
ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo, out bool wasFullyInferred)
{
Debug.Assert(method.IsExtensionMethod);
Debug.Assert((object)thisType != null);

if (!method.IsGenericMethod || method != method.ConstructedFrom)
{
wasFullyInferred = true;
return method;
}

// We never resolve extension methods on a dynamic receiver.
if (thisType.IsDynamic())
{
wasFullyInferred = false;
return null;
}

Expand Down Expand Up @@ -158,6 +161,7 @@ private static MethodSymbol InferExtensionMethodTypeArguments(MethodSymbol metho

if (typeArgs.IsDefault)
{
wasFullyInferred = false;
return null;
}

Expand Down Expand Up @@ -215,12 +219,14 @@ private static MethodSymbol InferExtensionMethodTypeArguments(MethodSymbol metho

if (!success)
{
wasFullyInferred = false;
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.All(static t => t.HasType);
if (!wasFullyInferred)
{
typeArgsForConstruct = typeArgs.ZipAsArray(
method.TypeParameters,
Expand Down
Loading
Loading