-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Adjust receiver in instance invocations #73501
base: features/roles
Are you sure you want to change the base?
Conversation
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.
(haven't looked at tests yet)
} | ||
else | ||
{ | ||
throw ExceptionUtilities.UnexpectedValue(receiver.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.
Is this going to throw for a receiver of unconstrained generic parameter type? #Resolved
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.
We currently don't perform extension member lookup on type parameters. When we do (planned), this section will need to be expanded.
@@ -108,6 +108,8 @@ private BoundExpression VisitIndexerAccess(BoundIndexerAccess node, bool isLeftO | |||
BoundIndexerAccess? oldNodeOpt, | |||
bool isLeftOfAssignment) | |||
{ | |||
rewrittenReceiver = AdjustReceiverForExtensionsIfNeeded(rewrittenReceiver, indexer); |
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.
We should probably have tests that VisitArgumentsAndCaptureReceiverIfNeeded
behaves correctly with our rewritten receiver. #Resolved
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 with interpolation handler arguments (fixed by adjusting the type of the interpolation "instance" placeholder). Thanks
} | ||
else if (receiver.Type.IsValueType) | ||
{ | ||
receiver = MakeTemp(receiver, temps, effects, RefKind.Ref); |
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.
Consider testing that this ref temp is going to report errors correctly in async methods for example. #Resolved
@jcouv It looks like there are legitimate test failures |
@@ -380,6 +379,8 @@ BoundExpression visitArgumentsAndFinishRewrite(BoundCall node, BoundExpression? | |||
rewrittenReceiver = null; | |||
} | |||
|
|||
rewrittenReceiver = AdjustReceiverForExtensionsIfNeeded(rewrittenReceiver, method); |
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 that whenever possible, this transformation should be done in the same code that uses ReferToTempIfReferenceTypeReceiver
. It looks like for this specific code path it will be possible. Basically, there should be one place where we transform receivers for a given language costruct. #Closed
{ | ||
var thisExtensionType = (SourceExtensionTypeSymbol)receiver.Type; | ||
Debug.Assert(thisExtensionType.UnderlyingInstanceField is not null); | ||
Debug.Assert(thisExtensionType.UnderlyingInstanceField.Type.Equals(member.ContainingType, TypeCompareKind.AllIgnoreOptions)); |
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(thisExtensionType.UnderlyingInstanceField is not null); | ||
Debug.Assert(thisExtensionType.UnderlyingInstanceField.Type.Equals(member.ContainingType, TypeCompareKind.AllIgnoreOptions)); | ||
|
||
return _factory.Field(receiver, thisExtensionType.UnderlyingInstanceField); |
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.
} | ||
else if (receiver.Type.IsValueType) | ||
{ | ||
receiver = MakeTemp(receiver, temps, effects, RefKind.Ref); |
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 didn't follow this. VisitArgumentsAndCaptureReceiverIfNeeded
saves the receiver into a temp when we need multiple re-uses of the receiver (either because we're doing some compound/complex operation or we'll need the receiver a second time as part of an interpolation handler).
But the purpose of the temp here is not multiple re-uses.
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 didn't follow this.
Let's sync up offline
} | ||
else if (receiver.Type.IsValueType) | ||
{ | ||
receiver = MakeTemp(receiver, temps, effects, RefKind.Ref); |
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.
|
||
var call = _factory.Call(null, | ||
_factory.WellKnownMethod(WellKnownMember.System_Runtime_CompilerServices_Unsafe__As_T) | ||
.Construct(ImmutableArray.Create<TypeSymbol>(receiver.Type, memberExtensionType)), |
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.
_factory.Syntax = receiver.Syntax; | ||
|
||
var call = _factory.Call(null, | ||
_factory.WellKnownMethod(WellKnownMember.System_Runtime_CompilerServices_Unsafe__As_T) |
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.
Added AdjustReceiver_AwaitArgument
. It errors. I left a PROTOTYPE comment as I didn't investigate closely whether we could make it work
} | ||
else if (receiver.Type.IsValueType) | ||
{ | ||
receiver = MakeTemp(receiver, temps, effects, RefKind.Ref); |
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.
We need to verify that readonly
semantics of the receiver is properly preserved (ref readonly
/in
parameters, readonly
fields, this
in readonly
method, etc.). The receiver should be copied as appropriate. Please make sure to add relevant tests. #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.
Added a set of readonly
tests. In non-extension scenarios, the work of cloning the receiver is handled in EmitInstanceCallExpression
(not in lowering). For extension scenarios, we assign the readonly
parameter/field/this
to a ref temp and the codegen for that assignment will handle that (in EmitAssignmentValue
).
} | ||
else if (receiver.Type.IsValueType) | ||
{ | ||
receiver = MakeTemp(receiver, temps, effects, RefKind.Ref); |
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.
We do need this capture. I'd tried without and ran into an assertion ("passing args byref should not clone them into temps") in EmitArgument.
@@ -906,6 +897,109 @@ static bool usesReceiver(BoundExpression argument) | |||
} | |||
} | |||
|
|||
private RefKind GetRefKindForCapturedReceiverTemp(BoundExpression rewrittenReceiver) |
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.
📝 This method is extracted, but the case for ref readonly
parameter was added (was blowing up in StoreToTemp
). Added a non-extensions repro
📝 this was causing problems (taking a rewritten receiver that is a sequence apart, but it's not the sequence this scenario had in mind) but the gymnastics were not necessary so simplified. Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ObjectOrCollectionInitializerExpression.cs:238 in 3534070. [](commit_id = 3534070, deletion_comment = True) |
BoundLocal? receiverTemp = null; | ||
BoundAssignmentOperator? assignmentToTemp = null; | ||
|
||
if (captureReceiverMode != ReceiverCaptureMode.Default || | ||
(requiresInstanceReceiver && arguments.Any(a => usesReceiver(a)))) | ||
(requiresInstanceReceiver && arguments.Any(a => usesReceiver(a))) || | ||
wasReceiverAdjusted) |
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.
The two "Transform" callers do a CanValueChangeBetweenReads
check and pass ReceiveCaptureMode.Default
when the value cannot change (or other capture modes when the value could change). For example, using a constant or this
in a struct as the receiver.
It seems unnecessary to evaluate Unsafe.As
twice in such cases, so we detect it and capture the temp anyways.
The extra temp did not appear to have any negative effect in the normal case (no compound assignment and no interpolation receiver capture), but I debated conditioning this only for those two "Transform" scnearios, by either adding a new capture mode and/or moving the CanValueChangeBetweenReads
check here.
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 comfortable with this approach. I think that the call sites that are trying to get some advantage/optimization by checking CanValueChangeBetweenReads
should deal with that rather than everybody else "suffer". Adding a new capture mode would be fine too.
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.
Also consider the following. An Unsafe.As
is an intrinsic and its calls will be completely removed from the code by JIT, whereas the unnecessary (from semantics point of view) locals will likely stay. Therefore, I am not convinced that we need to worry about those extra calls.
// PROTOTYPE if we decide to allow readonly members in extensions, we'll need to revisit this | ||
if (refKind is RefKind.RefReadOnly) | ||
{ | ||
refKind = RefKind.Ref; |
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(!receiver.Type.IsTypeParameter()); | ||
var refKind = GetRefKindForCapturedReceiverTemp(receiver, useNoneForReferenceType: true); | ||
// PROTOTYPE if we decide to allow readonly members in extensions, we'll need to revisit this | ||
if (refKind is RefKind.RefReadOnly) |
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.
RedKind.In == RefKind.RefReadonly, so no extra handling needed. A test covers both.
Will add the assertion and a test for out
@jcouv It looks like there are legitimate test failures |
@@ -3754,7 +3755,8 @@ static ParameterSymbol getCorrespondingParameter(in MemberAnalysisResult result, | |||
case BoundInterpolatedStringArgumentPlaceholder.InstanceParameter: | |||
Debug.Assert(receiver!.Type is not null); | |||
refKind = RefKind.None; | |||
placeholderType = receiver.Type; | |||
bool useExtensionInstance = methodResult.Member.ContainingType.IsExtension && !receiver.Type.IsExtension; | |||
placeholderType = useExtensionInstance ? methodResult.Member.ContainingType : receiver.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.
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.
That breaks a number of existing interpolation tests (such as StructReceiver_Lvalue_04
) because we need the placeholder type and the filler type to match. But for example, T
(value) and ILogger
(placeholder) don't match in that test.
@@ -68,7 +68,6 @@ private LocalDefinition EmitAddress(BoundExpression expression, AddressKind addr | |||
|
|||
if (expression.Type.IsValueType) | |||
{ | |||
|
|||
if (!HasHome(expression, addressKind)) |
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.
CodeFlow is bungling this comment. It shows up in the middle of In reply to: 2125179623 Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:17517 in 8cdb571. [](commit_id = 8cdb571, deletion_comment = True) |
If you look at the diff between commits 6 and 4, the comment is on the deleted IL verification right after the line 17473. If that IL was changed between the commits, I would find it useful to see the impact of the changes on it. However with validation deleted I cannot tell anything. I also see many more IL validation removed in the same diff. In general, given the nature of the changes, I find IL verification very useful/interesting. Therefore, if we have IL verification for the like scenario elsewhere, I think it is fine to get rid of duplication. Otherwise, I would keep verification for unique cases. In reply to: 2125282065 Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:17517 in 8cdb571. [](commit_id = 8cdb571, deletion_comment = True) |
method.CheckConstraints( | ||
new ConstraintsHelper.CheckConstraintsArgs(_compilation, _compilation.Conversions, temp.Syntax.GetLocation(), _diagnostics)); | ||
|
||
var call = _factory.Call(null, method, temp); |
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 6), tests are not looked at |
I am not sure what we were trying to accomplish with this constructor. It looks like the value that we pass is not used in any meaningful way. #Closed Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:23422 in 0032ca5. [](commit_id = 0032ca5, deletion_comment = False) |
When we are testing behavior around structures, I think we can benefit with some strengthening. We are verifying thar receiver's value is correct, but it would also be good to verify that receiver's location is correct. For example, this can be achieved by incrementing the
|
Are we still hitting all the interesting code paths after the logic around In reply to: 2122988191 Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:667 in 8cdb571. [](commit_id = 8cdb571, deletion_comment = False) |
Why is this scenario interesting? Regardless of what we do with receiver, this call always operates on a copy because Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:43993 in 0032ca5. [](commit_id = 0032ca5, deletion_comment = False) |
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)
Yes, we're hitting:
In reply to: 2127594065 Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:667 in 8cdb571. [](commit_id = 8cdb571, deletion_comment = False) |
This just adds a side-effect to the constructor which makes it obvious in the expectedOutput that the created instances were captured and we didn't evaluate the constructors more than we should. In reply to: 2127533873 Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:23422 in 0032ca5. [](commit_id = 0032ca5, deletion_comment = False) |
In reply to: 2127582150 Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:41309 in 0032ca5. [](commit_id = 0032ca5, deletion_comment = False) |
tempsOpt.Add(temp.LocalSymbol); | ||
Debug.Assert(temp.Type is not null); | ||
|
||
MethodSymbol method = _factory.WellKnownMethod(WellKnownMember.System_Runtime_CompilerServices_Unsafe__As_T) |
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, AdjustReceiver_Invocation_ObjectCreation_MissingUnsafeAs
This PR is on hold pending confirmation on the Unsafe.As approach
Relates to test plan #66722