-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Support null-conditional operator when the return value is a pointer type (including function pointers) #80694
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
Support null-conditional operator when the return value is a pointer type (including function pointers) #80694
Conversation
…havior Co-authored-by: CyrusNajmabadi <[email protected]>
- Modified Binder_Expressions.cs to allow pointer types in null-conditional expressions - Changed check from IsPointerOrFunctionPointer() to IsFunctionPointer() to allow regular pointers - Added condition to skip Nullable<T> wrapping for pointer types - Pointers can represent null as the zero value, so no need for Nullable<T> - Function pointers are still correctly rejected - Added comprehensive tests covering various pointer scenarios - All existing tests continue to pass Co-authored-by: CyrusNajmabadi <[email protected]>
| } | ||
|
|
||
| // The resulting type must be either a reference type T or Nullable<T> | ||
| // The resulting type must be either a reference type T, Nullable<T>, or a pointer 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.
@dotnet/roslyn-compiler i think this is correct. The spec says this: https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1177-null-conditional-member-access
A null_conditional_member_access expression E is of the form P?.A. Let T be the type of the expression P.A. The meaning of E is determined as follows:
Otherwise the type of E is T, and the meaning of E is the same as the meaning of:
- ((object)P == null) ? null : P.A
- Except that P is evaluated only once.So my reading from teh spec is that this should be legal. #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.
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1288-null-conditional-member-access has revised the wording somewhat but I think Cyrus's interpretation still holds.
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 latest docs show this:
Let T be the type of the expression P.A.
...
Otherwise the type of E is T, and the meaning of E is the same as the meaning of:
((object)P == null) ? (T)null : P.A
Except that P is evaluated only once.
| graph, symbol); | ||
| } | ||
|
|
||
| [Fact] |
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.
@copilot these should have WorkItem attributes on them. The WorkItem should only have one argument. The url to the issue this is a test for. #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 WorkItem attributes to all 7 test methods with the issue URL. (commit: work in progress)
Added WorkItem attribute with issue URL to all 7 test methods as requested in code review. Co-authored-by: CyrusNajmabadi <[email protected]>
…ithub.com/dotnet/roslyn into copilot/support-null-conditional-operator
|
@dotnet/roslyn-compiler ptal. |
|
I think I agree with Cyrus's interpretation of the spec. I was suspicious that this scenario went from disallowed by spec to allowed by spec when the spec was rewritten in csharpstandard. I went and looked, and my interpretation is the same with both the old and new spec language. See the old spec for
It looks like past interpretations were that pointers are non-nullable value types. On that reasoning, it's invalid for a conditional-access's value to be of a pointer type, because However, both the old and new specs say that pointers are not value types:
I think the motivation for actually making the change is low. I don't think it's clear that the spec authors had pointers in mind when the |
|
@dotnet/roslyn-compiler ptal |
|
I am not sure about interpretation given above for the following quote:
I suspect the intent was actually to say that pointers are "special" value types (value types because instances aren't heap allocated and aren't GC tracked). Compiler treats them as value types with restrictions outlined above. Since we are not attempting to change that, and changing that is likely to have some unintended consequences. I think the specific behavior change for null-conditional operator should go through the regular language change process: proposal, LDM approval, etc. #Closed |
| // - access cannot be a restricted type | ||
| if ((!accessType.IsReferenceType && !accessType.IsValueType) || accessType.IsPointerOrFunctionPointer() || accessType.IsRestrictedType()) | ||
| // Note: Regular pointers are allowed because they can represent null (as the zero value). | ||
| if ((!accessType.IsReferenceType && !accessType.IsValueType) || accessType.IsFunctionPointer() || accessType.IsRestrictedType()) |
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 am unfamiliar with function pointers. If they are the same here and the transformation is equally legal, I am fine allowing. I'll let those with experience with them weigh in here.
|
Highly agreed with Aleksey. Neither bullet in the spec is a valid transformation for a pointer type. You can't have |
|
The suggested change doesn't involve permitting the receiver type of the conditional access to be a pointer, only the result type. In that scenario, the transformation indicated by the spec appears valid to me. .NET Lab. class C
{
public int* ptr;
}
C? c = null;
var ptr1 = c?.ptr;
// if you assume pointers are "not value types", then the spec defines the 'ptr1' case as equivalent to the following:
var ptr2 = (object)c == null ? null : c.ptr;As far as whether pointers are value types in the "language sense". I defer to the rest of the team's judgment on that. I agree that if it is felt strongly enough that this change should be made, then it should go through LDM process. Thanks |
I'm a bit confused. I'm not proposing a language change here. I'm saying I think this falls out from the parts of the spec I linked to. Looking at the original report, the user has: I don't see where Instead, I'm saying that the spec says that the above is interpreted as
Here there is no casting of the pointer. Nor are Nullable value types involved. |
I'm confused by this. The spec has three bullets here.
I'm saying that since neither the first nor second bullets apply, this falls into the third. And the transformation suggested by the third is legal. A pointer is neither a type parameter nor a non Nullable value type. And the second bullet point is clearly talking about what we normally think of as a value type, and would then get wrapped with T?, which definitely doesn't apply here. Afaict, the first two bullet points are for the cases where something interesting has to happen. The last bullet covers everything else. I don't think it is correct for either to be interpreted as applying to pointers. Nor do I think that was the intent of the spec. Clearly the initial bullets are about handling the case where null is not allowed or special. Everything else gets normal handling since null is fine there. |
| } | ||
| """; | ||
| var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll); | ||
| comp.VerifyEmitDiagnostics(); |
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.
| static unsafe void M(Test t) | ||
| { | ||
| int* p = t?.Value; | ||
| var v = t?.Value; |
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.
just validating that it works even with a var assignment.
| { | ||
| static unsafe void M(A a) | ||
| { | ||
| a?.DoSomething(); // Statement context - method call works |
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.
no. adjusted.
| static unsafe void M(Test t) | ||
| { | ||
| delegate*<int, string> f = t?.FPtr; | ||
| var g = t?.FPtr; |
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.
sure. removed.
| } | ||
|
|
||
| [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")] | ||
| public void FunctionPointerReturnType_WithReturn() |
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.
removed.
…l-conditional-operator
| { | ||
| unsafe static void M1(A a) | ||
| { | ||
| byte* ptr = a?.Ptr; |
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 looks like we are lacking tests for scenarios when receiver is Nullable<T>. Also, please make sure we validate IL for a method like:
unsafe static byte* M(A a)
{
return <receiver>?.Ptr;
}
Covering the following matrix:
- Receiver type class
- Receiver type
Nullable<T> - Receiver is a parameter
- Receiver is a method call #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.
Sure 4422eb1
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 looks like the following combination is not covered:
- Receiver type
Nullable<T> - Receiver is a method call
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.
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 PointerReturnType_NullableValueTypeMethodCallReceiver test that combines Nullable receiver with a method call. The test verifies both null and non-null cases with IL verification. (commit pending)
|
Done with review pass (commit 20) #Closed |
|
Done with review pass (commit 24) #Closed |
Added PointerReturnType_NullableValueTypeMethodCallReceiver test that combines: - Receiver type Nullable<T> - Receiver is a method call (GetA(a)?.Ptr) Tests both null and non-null cases with IL verification Co-authored-by: CyrusNajmabadi <[email protected]>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Test added. |
AlekseyTs
left a comment
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 25)
Summary
This PR adds support for the null-conditional operator (
?.) when the return value is a pointer type (including function pointers), fixing the issue where code likebyte* ptr = x?.Ptr;ordelegate*<int, void> fptr = x?.FPtr;was incorrectly rejected with error CS8978. Also validates that conditional assignment (x?.Ptr = value) works correctly with pointers.Changes Made
1. Modified
Binder_Expressions.cs:IsPointerType()toIsPointerOrFunctionPointer()for consistent handlingNullable<T>wrapping for both pointer types since they can already represent null2. Added 13 comprehensive tests in
NullConditionalAssignmentTests.cs:PointerReturnType_Simple: Basic functionality test withbyte*PointerReturnType_ClassReceiver: Tests with class receiverPointerReturnType_Parameter_ClassReceiver: Tests with parameter class receiverPointerReturnType_NullableValueTypeReceiver: Tests with Nullable value type receiverPointerReturnType_MethodCallReceiver: Tests with method call receiverPointerReturnType_NullableValueTypeMethodCallReceiver: Tests with Nullable method call receiver (comprehensive coverage of Nullable scenarios)PointerReturnType_WithUsage: Runtime behavior verification with IL verification showing correct code generation for both null and non-null casesPointerReturnType_IntPointer: Tests withint*including semantic model type verificationPointerReturnType_VoidPointer: Tests withvoid*PointerReturnType_Chained: Chained access scenariosFunctionPointerReturnType_Allowed: Basic function pointer testFunctionPointerReturnType_MultipleParameters: Function pointer with multiple parametersFunctionPointerReturnType_Execution: Execution test for function pointers demonstrating both null and non-null behaviorPointerConditionalAssignment: Tests conditional assignment (a?.Ptr = value) with pointers, validates assignment only occurs when receiver is not null, includes IL verificationFunctionPointerConditionalAssignment: Tests conditional assignment with function pointers, validates assignment behavior, includes IL verification[WorkItem("https://github.com/dotnet/roslyn/issues/7502")]attributesTechnical Details
Nullable<T>wrapping neededx?.y = z) works correctly with pointers and function pointers - assignment only occurs when receiver is not null💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.