Skip to content

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Jun 17, 2025

Relates to test plan #76130

@jcouv jcouv self-assigned this Jun 17, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Jun 17, 2025
@jcouv jcouv force-pushed the extensions-interceptors branch from 6469d19 to 34c2420 Compare June 17, 2025 18:10
@jcouv jcouv marked this pull request as ready for review June 17, 2025 21:08
@jcouv jcouv requested a review from a team as a code owner June 17, 2025 21:08
@jcouv jcouv requested review from jjonescz and AlekseyTs June 19, 2025 15:27
}
""";
var comp = CreateCompilation([source, interceptors, s_attributesSource], parseOptions: RegularNextWithInterceptors);
comp.VerifyEmitDiagnostics();
Copy link
Member

@jjonescz jjonescz Jun 20, 2025

Choose a reason for hiding this comment

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

Should we verify execution to see that the interception succeeded?

Applies to other new tests that don't have compilation errors. #Resolved


static ParameterSymbol? getReceiverParameter(MethodSymbol method, bool classicExtensionNotInvokedAsStatic)
{
if (method.IsExtensionMethod && classicExtensionNotInvokedAsStatic)
Copy link
Member

Choose a reason for hiding this comment

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

Do we have tests for extension methods that are invoked as static?

interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null;
switch (methodThisParameter, interceptorThisParameterForCompare)
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod);
ParameterSymbol? interceptorThisParameterForCompare = getReceiverParameter(interceptor, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod || needToReduceInterceptor);
Copy link
Member

@jjonescz jjonescz Jun 20, 2025

Choose a reason for hiding this comment

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

Why does invokedAsExtensionMethod play a role in determining the receiver parameter of the interceptor? #Resolved

@@ -141,6 +141,7 @@ private void InterceptCallAndAdjustArguments(
bool invokedAsExtensionMethod,
Syntax.SimpleNameSyntax? nameSyntax)
{
// Note: the extension method rewriter comes after the local rewriter, so we're still dealing with skeleton methods here
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

skeleton methods here

"declaration form of methods"? #Closed


if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare))
MethodSymbol? interceptorForCompare =
interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod ? interceptor.TryGetCorrespondingExtensionImplementationMethod() :
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod

Is condition interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod ever true? #Closed


if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare))
MethodSymbol? interceptorForCompare =
interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod ? interceptor.TryGetCorrespondingExtensionImplementationMethod() :
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

interceptor.TryGetCorrespondingExtensionImplementationMethod()

This looks suspicious. In general getting implementation method is moving us to opposite direction by comparison to what ReducedExtensionMethodSymbol.Create does on the other branch. #Closed

var interceptorThisParameterForCompare = needToReduce ? interceptor.Parameters[0] :
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null;
switch (methodThisParameter, interceptorThisParameterForCompare)
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

classicExtensionNotInvokedAsStatic

I do not see any benefits from this rename. I think we should stick to the usual name invokedAsExtensionMethod that is well understood and is used throughout the codebase. Different name leads to believe that we are dealing with a different concept. #Closed

var interceptorThisParameterForCompare = needToReduce ? interceptor.Parameters[0] :
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null;
switch (methodThisParameter, interceptorThisParameterForCompare)
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

invokedAsExtensionMethod

This value feels wrong. methodThisParameter didn't depend on invokedAsExtensionMethod before and the renamed value methodReceiverParameter shouldn't depend on it too, meaning we should always pass false here. Otherwise, it looks like there is a behavior change here for a legacy scenario, however, there is no indication (in a comment or in a PR description) that there is an intent to fix a bug. #Closed

interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null;
switch (methodThisParameter, interceptorThisParameterForCompare)
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod);
ParameterSymbol? interceptorThisParameterForCompare = getReceiverParameter(interceptor, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod || needToReduceInterceptor);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

invokedAsExtensionMethod || needToReduceInterceptor

Combination of these values also looks suspicious and there is no explanation provided why this is the right condition to use for the entire breadth of scenarios. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jun 20, 2025

            interceptor = interceptor.Construct(typeArgumentsBuilder.ToImmutableAndFree());

Is this going to do the right thing for a new extension method? #Closed


Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:175 in 34c2420. [](commit_id = 34c2420, deletion_comment = False)

if (!argumentRefKindsOpt.IsDefault)
{
argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisRefKind);
}
}
}

method = interceptor;
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

method = interceptor;

Let's say interceptor is an instance extension and method is invoked as extension. It looks like we are going to use implementation form of interceptor to match the signature to method. So far good. But then here we are replacing method with interceptor (the declaration form). How are they going to align in the BoundCall that will be created. There would be no receiver and too many arguments in that call. I am failing to connect the dots. What am I missing? #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.

We're constructing a busted BoundCall from lowering (lacks receiver, has extra argument) but the ExtensionMethodReferenceRewriter hid the problem. I'll add an assertion there. Thanks for catching this

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jun 20, 2025

        Debug.Assert(!interceptor.ContainingType.IsGenericType);

It is not clear what guarantees this for new extensions. And it is not clear why would we want to have this restriction for new extensions, given that implementation methods necessary satisfy the restriction. BTW, it looks like we don't have tests for generic extension blocks. #Closed


Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:153 in 34c2420. [](commit_id = 34c2420, deletion_comment = False)

@@ -141,6 +141,7 @@ private void InterceptCallAndAdjustArguments(
bool invokedAsExtensionMethod,
Syntax.SimpleNameSyntax? nameSyntax)
{
// Note: the extension method rewriter comes after the local rewriter, so we're still dealing with skeleton methods here
if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor))
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

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

if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor))

This function went through some rather complicated changes with no explanation why the logic should be this way or that way. I am guessing that the complexity is coming from a desire to reconcile an interception of a classic extension invocation by a new style instance extension method. I am suggesting to use an alternative implementation strategy. Starting with the following:

  1. Revert all the changes
  2. Right after this line succeeds, if interceptor is a new extension, replace it with implementation form. There is no taboo against using implementation forms during lowering. Especially in a case like this, where we are not dealing with a method explicitly referenced by a user at the call site.
  3. Adjust method.TryGetThisParameter(out var methodThisParameter); to get extension parameter for instance extensions.
  4. Let the rest of the method take its course and do what it used to do.

If that doesn't address some scenarios, I will be happy to discuss them and suggest appropriate adjustments to the strategy. #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 approach is much simpler indeed :-) I'd stuck myself with constraint of leaving the ExtensionMethodReferenceRewriter deal with replacement :-/

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 1), test are not reviewed.

@@ -73,6 +73,8 @@ public static BoundNode VisitCall(BoundTreeRewriter rewriter, BoundCall node)

static BoundExpression visitArgumentsAndFinishRewrite(BoundTreeRewriter rewriter, BoundCall node, BoundExpression? rewrittenReceiver)
{
Debug.Assert(!node.Method.GetIsNewExtensionMember() || node.Method.IsStatic || node.ReceiverOpt is not null);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 24, 2025

Choose a reason for hiding this comment

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

!node.Method.GetIsNewExtensionMember()

Is this condition necessary? #Closed

@@ -147,6 +147,11 @@ private void InterceptCallAndAdjustArguments(
return;
}

if (interceptor.GetIsNewExtensionMember() && interceptor.TryGetCorrespondingExtensionImplementationMethod() is { } implementationMethod)
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 24, 2025

Choose a reason for hiding this comment

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

interceptor.TryGetCorrespondingExtensionImplementationMethod() is { } implementationMethod

It doesn't feel like we can continue otherwise. If there is a guarantee that the implementation method is present (for example, it is always defined in the same source module), then we should throw unreachable. If there is no guarantee, then we should rep[ort an error and recover. #Closed

}
""";
var comp = CreateCompilation([source, interceptors, s_attributesSource], parseOptions: RegularPreviewWithInterceptors);
comp.VerifyEmitDiagnostics(
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 24, 2025

Choose a reason for hiding this comment

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

comp.VerifyEmitDiagnostics(

Is it still possible to execute the code? #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 2)

@@ -73,6 +73,8 @@ public static BoundNode VisitCall(BoundTreeRewriter rewriter, BoundCall node)

static BoundExpression visitArgumentsAndFinishRewrite(BoundTreeRewriter rewriter, BoundCall node, BoundExpression? rewrittenReceiver)
{
Debug.Assert(node.Method.MethodKind != MethodKind.Ordinary || node.Method.IsStatic || node.ReceiverOpt is not null);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 25, 2025

Choose a reason for hiding this comment

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

node.Method.MethodKind != MethodKind.Ordinary

It is not clear to me why this condition is necessary. Wouldn't we want node.Method.IsStatic || node.ReceiverOpt is not null to be true always? #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 3)

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 4)

@jcouv jcouv requested a review from jjonescz June 25, 2025 20:11
@jcouv jcouv merged commit d3350fa into dotnet:main Jun 26, 2025
24 checks passed
@jcouv jcouv deleted the extensions-interceptors branch June 26, 2025 17:53
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jun 26, 2025
@RikkiGibson RikkiGibson modified the milestones: Next, 18.0 P1 Aug 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Feature - Extension Everything The extension everything feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants