-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Extensions: Bind invocations and member accesses #77172
Conversation
@@ -928,6 +928,8 @@ protected BoundCall MakeQueryInvocation(CSharpSyntaxNode node, BoundExpression r | |||
Debug.Assert(receiver.Type is object || ultimateReceiver.Type is null); | |||
if ((object?)ultimateReceiver.Type == null) | |||
{ | |||
Debug.Assert(ultimateReceiver.Kind != BoundKind.MethodGroup || ultimateReceiver.HasAnyErrors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 See previous discussion on this change #73239 (comment)
ac436b7
to
ae9a409
Compare
ae9a409
to
245479d
Compare
@@ -1975,7 +1975,6 @@ protected override void AfterMembersCompletedChecks(BindingDiagnosticBag diagnos | |||
// The type 'Microsoft.CodeAnalysis.EmbeddedAttribute' must be non-generic, internal, sealed, non-static, have a parameterless constructor, inherit from System.Attribute, and be able to be applied to any type. | |||
diagnostics.Add(ErrorCode.ERR_EmbeddedAttributeMustFollowPattern, GetFirstLocation()); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like there are meaningful changes in this file. Consider reverting it to its previous state. #Closed
MutableTypeMap? substitution = null; | ||
bool result = CanUnifyHelper(parameterType, type, onlySubstituteInLHS: true, ref substitution); | ||
#if DEBUG | ||
if (result && (parameterType is not null && type is not null)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#endif | ||
|
||
PooledHashSet<NamedTypeSymbol>? visited = null; | ||
var baseType = type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of comments:
- It looks like we are not handling scenario when
type
is a generic type parameter. - It is not clear to me why we are not using the same approach that
ReducedExtensionMethodSymbol.Create
uses to check compatibility and a general type argument inference algorithm like inReducedExtensionMethodSymbol.InferExtensionMethodTypeArguments
. We have a parameter and we have a type for the corresponding argument. What is the reason to use different approach? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR re-uses the algorithm from the roles branch as we'd discussed. But in email I suggested that we use method type inference here instead of type unification, which is what ReducedExtensionMethodSymbol
does. I was planning to do that in a subsequent PR, but am happy to include that change here.
Yes, this PR doesn't handle type parameter receivers (it's explicitly blocked in places, marked with PROTOTYPE). I probably can revisit that after change to type inference.
if (lookupResult.IsMultiViable) | ||
{ | ||
return BindMemberOfType(node, right, rightName, rightArity, indexed, boundLeft, typeArgumentsSyntax, typeArguments, lookupResult, BoundMethodGroupFlags.None, diagnostics: diagnostics); | ||
return BindMemberOfType(node, right, rightName, rightArity, indexed, boundLeft, typeArgumentsSyntax, typeArguments, lookupResult, BoundMethodGroupFlags.SearchExtensions, diagnostics: diagnostics); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the flag should be set unconditionally, unless we can identify a type member access scenario where we shouldn't resolve extensions.
In short:
Previously, we'd only set the flag in instance member access scenarios. It would generally indicate that the member group came from an instance member access, with a couple of limited exceptions (see below).
Now, we also want to set this flag for method groups resulting for a type member access. The previous exceptions are not applicable and we haven't found new exceptions that would require using this flag conditionally here.
Setting this flag on type member access enables invocation scenarios (ResolveMethodGroup
should call ResolveExtensions
, see methodGroup.SearchExtensions
check) and natural function type scenarios (GetUniqueSignatureFromMethodGroup
should inspect extensions in a future PR).
Note: although LookupOptions
does have an IncludeExtensionMethods
flag, I don't think it's relevant here (it's only used for lookup APIs in the semantic model). The options
chosen in BindMemberAccessWithBoundLeft
don't need to use it.
My thought process:
This flag gets stored into the BoundMethodGroup
in cases where one is returned:
- in
BindMemberOfType
, we either return a method group with this flag (when a method group is found or no result), or a non-method result such as a type, property access, event access, field access (which don't use this flag) - in
MakeBoundMethodGroupAndCheckOmittedTypeArguments
always returns a method group with this flag
The flag indicates that subsequent usage of the method group should look at extensions, because it came from a member access where we want to allow extension resolution.
- if we don't have a member access, we don't use this flag. For example, we don't use it when binding an identifier (see
ConstructBoundMemberGroupAndReportOmittedTypeArguments
call inBindIdentifier
). - for a type receiver, we previously didn't set this flag
- for the remaining cases, we generally set the flag for a member access with two exceptions: in
BindMemberAccessWithBoundLeftTypeOrInstance
if!searchExtensionsIfNecessary
(only usedfor binding interpolation handlers, which purposefully exclude extension resolution) and
leftIsBaseReference(no extension resolution on
base` access).
We want to set the flag for case 2 and have not yet identified any exceptions for that scenario.
} | ||
|
||
return MakeBoundMethodGroupAndCheckOmittedTypeArguments(boundLeft, rightName, typeArguments, lookupResult, | ||
flags: BoundMethodGroupFlags.SearchExtensions, node, typeArgumentsSyntax, diagnostics); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Note: we're resolving without arguments, which means we're not treating the member access as invoked | ||
var resolution = this.ResolveExtension( | ||
syntax, name, analyzedArguments: null, receiver, typeArgumentsOpt, options: OverloadResolution.Options.None, | ||
returnRefKind: default, returnType: null, withDependencies: useSiteInfo.AccumulatesDependencies); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
#nullable disable | ||
|
||
private BoundExpression BindMemberAccessWithBoundLeftCore( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was the "Instance" part of the original name inaccurate? I think we need to come up with a clearer, more descriptive name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From offline discussion, yes the issue was with "Instance". The bound left may not be an instance. I'll take a stab at improving the name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up reverting this. I still think the name isn't great, but it's not essential for our present purpose
} | ||
else if (firstResult.IsEmpty) | ||
{ | ||
firstResult = extensionResult; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
firstResult.IsEmpty
indicates that don't have anything to free (in particular, no analyzed arguments). I'll add an assertion to clarify.
{ | ||
if (extensionResult.IsNonMethodExtensionMember(out _)) | ||
{ | ||
return extensionResult; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (extensionMethodResult.HasAnyApplicableMethod) | ||
{ | ||
firstResult.Free(); | ||
return extensionMethodResult; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug.Assert(extensionMemberAccess.Kind != BoundKind.MethodGroup); | ||
|
||
extensionMemberAccess = CheckValue(extensionMemberAccess, BindValueKind.RValue, diagnostics); | ||
BoundExpression extensionMemberInvocation = BindInvocationExpression(syntax, expression, methodName, extensionMemberAccess, analyzedArguments, diagnostics); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
Debug.Assert(!resolution.IsNonMethodExtensionMember(out _)); | ||
return resolution; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean to remove return resolution;
above? Otherwise this code can be moved to the else
block. #Closed
@@ -33,9 +33,7 @@ static void Main(string[] args) | |||
string expectedOperationTree = @" | |||
IInvalidOperation (OperationKind.Invalid, Type: ?, IsInvalid) (Syntax: 'Console.WriteLine2()') | |||
Children(1): | |||
IInvalidOperation (OperationKind.Invalid, Type: ?, IsInvalid) (Syntax: 'Console.WriteLine2') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is due to the change in tryBindMemberAccessWithBoundTypeLeft
(Binder_Expression.cs line 7916)
See explanation: #68629 (comment)
Also, letting that scenario return null instead of a method group breaks many scenarios (for example, StaticMethodInvocation_Simple
would error saying that C
doesn't contain a definition for M
)
Done with review pass (commit 1) |
return extension.Construct(typeArguments); | ||
} | ||
|
||
static bool checkConstraints(Symbol symbol, ImmutableArray<TypeParameterSymbol> typeParams, ImmutableArray<TypeWithAnnotations> typeArgs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return extension.Construct(typeArguments); | ||
} | ||
|
||
static bool checkConstraints(Symbol symbol, ImmutableArray<TypeParameterSymbol> typeParams, ImmutableArray<TypeWithAnnotations> typeArgs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -184,6 +184,8 @@ internal override UseSiteInfo<AssemblySymbol> GetUseSiteInfo() | |||
internal sealed override bool IsRecordStruct => false; | |||
internal sealed override bool HasPossibleWellKnownCloneMethod() => false; | |||
|
|||
internal sealed override ParameterSymbol ExtensionParameter => throw ExceptionUtilities.Unreachable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure how this error is relevant to the compiled scenario. #Closed Refers to: src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs:6039 in bc0b7ed. [](commit_id = bc0b7ed, deletion_comment = False) |
|
||
static bool tryResolveExtensionMember(Binder binder, SyntaxNode expression, string memberName, AnalyzedArguments? analyzedArguments, BoundExpression left, | ||
ImmutableArray<TypeWithAnnotations> typeArgumentsWithAnnotations, OverloadResolution.Options options, RefKind returnRefKind, in CallingConventionInfo callingConvention, | ||
TypeSymbol? returnType, BindingDiagnosticBag diagnostics, ExtensionScope scope, out MethodGroupResolution extensionResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with review pass (commit 5) |
lookupResult, left.Type!, memberName, arity, | ||
basesBeingResolved: null, lookupOptions, originalBinder: binder, ref useSiteInfo); | ||
|
||
diagnostics.Add(expression, useSiteInfo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is where the problem is. This is where we are dropping use-site in case diagnostics is dropped below. And the diagnostics is dropped for a good reason. I think use-site diagnostics should be collected separately on the ResolveExtension
level. I.e. it should be passed in as a parameter and we shouldn't be merging it into diagnostic bags that might be dropped. #Closed
BoundExpression left, | ||
ImmutableArray<TypeWithAnnotations> typeArgumentsWithAnnotations, | ||
OverloadResolution.Options options, | ||
RefKind returnRefKind, | ||
TypeSymbol returnType, | ||
TypeSymbol? returnType, | ||
bool withDependencies, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this place shouldn't change. We are not dropping Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs:8681 in 8462cf7. [](commit_id = 8462cf7, deletion_comment = True) |
Let's discuss next week. I don't understand why we would want some use-site diagnostics attached to the result, but not others. I think we should treat all use-site diagnostics in In reply to: 2660477061 Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs:8681 in 8462cf7. [](commit_id = 8462cf7, deletion_comment = True) |
There is a good rational behind this argument. At the same time it looks like In reply to: 2661981228 Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs:8681 in 8462cf7. [](commit_id = 8462cf7, deletion_comment = True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (commit 8)
@jjonescz for second review. Thanks |
This is mostly re-using logic from the roles branch.
The core change/method is
ResolveExtension
(inBinder_Expression.cs
) and the various callers.It's used when resolving method groups (binding invocations, MakeMemberAccessValue, ...) and binding member accesses (BindMemberAccessWithBoundLeft and BindInstanceMemberAccess)
The
MethodGroupResolution
which it returns can now also represent a non-method result (ie. we found a property instead of methods).Function type scenarios are out-of-scope for this PR.
Relates to test plan #76130
Note that we don't need to resolve to a non-method extension member during conversions.
Here are the relevant sections of the spec that justify this understanding:
The Member Lookup has concept of a member being "invoked":
But in Method group conversions, although treat the conversion as an invocation:
The method group conversion only kicks in when we have already determined that we have a method group:
In short, a conversion to a delegate type does not cause the expression to be treated as "invoked", although we process it somewhat like an invocation.
Relates to test plan #66722