Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 14, 2025

  • Understand the issue: null-conditional operator with pointer return types
  • Add test cases to verify current behavior
  • Identified that pointers are currently rejected in value context
  • Determined the fix: allow pointers in null-conditional expressions
  • Implement the fix in Binder_Expressions.cs
  • Update and add comprehensive test cases
  • Run tests to verify the fix works correctly
  • Clean up temporary test files
  • Run broader test suite to ensure no regressions (all 205+ tests passing)
  • Code review completed and feedback addressed
  • Added WorkItem attributes to all test methods
  • Allow function pointers same as regular pointers
  • Updated comments to clarify pointer type treatment
  • Enhanced tests with IL verification, semantic model checks, and execution tests
  • Added conditional assignment tests for pointers and function pointers
  • Added tests for Nullable receivers including method call scenarios

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 like byte* ptr = x?.Ptr; or delegate*<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:

  • Removed the check that was rejecting function pointers
  • Changed IsPointerType() to IsPointerOrFunctionPointer() for consistent handling
  • Updated comments to clarify that both regular pointers and function pointers are allowed
  • Added detailed comment explaining that pointers are not value types in the language sense, but are treated as value types due to historical compiler implementation
  • Added condition to skip Nullable<T> wrapping for both pointer types since they can already represent null

2. Added 13 comprehensive tests in NullConditionalAssignmentTests.cs:

  • PointerReturnType_Simple: Basic functionality test with byte*
  • PointerReturnType_ClassReceiver: Tests with class receiver
  • PointerReturnType_Parameter_ClassReceiver: Tests with parameter class receiver
  • PointerReturnType_NullableValueTypeReceiver: Tests with Nullable value type receiver
  • PointerReturnType_MethodCallReceiver: Tests with method call receiver
  • PointerReturnType_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 cases
  • PointerReturnType_IntPointer: Tests with int* including semantic model type verification
  • PointerReturnType_VoidPointer: Tests with void*
  • PointerReturnType_Chained: Chained access scenarios
  • FunctionPointerReturnType_Allowed: Basic function pointer test
  • FunctionPointerReturnType_MultipleParameters: Function pointer with multiple parameters
  • FunctionPointerReturnType_Execution: Execution test for function pointers demonstrating both null and non-null behavior
  • PointerConditionalAssignment: Tests conditional assignment (a?.Ptr = value) with pointers, validates assignment only occurs when receiver is not null, includes IL verification
  • FunctionPointerConditionalAssignment: Tests conditional assignment with function pointers, validates assignment behavior, includes IL verification
  • All tests have [WorkItem("https://github.com/dotnet/roslyn/issues/7502")] attributes

Technical Details


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits October 14, 2025 10:20
- 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]>
Copilot AI changed the title [WIP] Support null-conditional operator for pointer types Support null-conditional operator when the return value is a pointer type Oct 14, 2025
Copilot AI requested a review from CyrusNajmabadi October 14, 2025 10:42
}

// 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.
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Oct 14, 2025

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

Copy link
Member

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.

Copy link
Member

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]
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Oct 14, 2025

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

Copy link
Contributor Author

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)

Copilot AI and others added 3 commits October 14, 2025 15:23
Added WorkItem attribute with issue URL to all 7 test methods as requested in code review.

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot AI requested a review from CyrusNajmabadi October 14, 2025 15:24
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review October 14, 2025 15:33
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner October 14, 2025 15:33
@CyrusNajmabadi
Copy link
Member

@dotnet/roslyn-compiler ptal.

@RikkiGibson
Copy link
Member

RikkiGibson commented Oct 14, 2025

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 ?.:

  • If T0 is a non-nullable value type, then the type of E is T0?, and the meaning of E is the same as

    ((object)P == null) ? (T0?)null : E1

    except that P is evaluated only once.

  • Otherwise the type of E is T0, and the meaning of E is the same as

    ((object)P == null) ? null : E1

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 int*? isn't valid.

However, both the old and new specs say that pointers are not value types:

Pointer types are a separate category of types. Unlike reference types and value types, pointer types do not inherit from object and no conversions exist between pointer types and object.

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 ?. feature was being drafted. But, if there isn't a strong pushback from the rest of the team, I don't have a problem with moving forward with this, as a bug-fix level change for a niche scenario.

@CyrusNajmabadi
Copy link
Member

@dotnet/roslyn-compiler ptal

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 19, 2025

I am not sure about interpretation given above for the following quote:

Pointer types are a separate category of types. Unlike reference types and value types, pointer types do not inherit from object and no conversions exist between pointer types and object.

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.

        public override bool IsReferenceType
        {
            get
            {
                return false;
            }
        }

        public override bool IsValueType
        {
            get
            {
                return true;
            }
        }

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())
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 19, 2025

Choose a reason for hiding this comment

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

accessType.IsFunctionPointer()

And why function pointers are treated differently? They are pointers as well. #Closed

Copy link
Member

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.

@333fred
Copy link
Member

333fred commented Oct 19, 2025

Highly agreed with Aleksey. Neither bullet in the spec is a valid transformation for a pointer type. You can't have int*?, and you can't have (object)pointerValue. This is a real language change, not a bug fix.

@RikkiGibson
Copy link
Member

RikkiGibson commented Oct 19, 2025

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

@CyrusNajmabadi
Copy link
Member

Highly agreed with Aleksey. Neither bullet in the spec is a valid transformation for a pointer type. You can't have int*?, and you can't have (object)pointerValue. This is a real language change, not a bug fix.

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:

var x = new A (); 
byte* ptr = x?.Ptr;

I don't see where int*? comes into things, or where there is a cast of a pointer to an object.

Instead, I'm saying that the spec says that the above is interpreted as

byte* ptr = ((object) x == null) ? null : x.Ptr

Here there is no casting of the pointer. Nor are Nullable value types involved.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Oct 19, 2025

Highly agreed with Aleksey. Neither bullet in the spec is a valid transformation for a pointer type. You can't have int*?, and you can't have (object)pointerValue. This is a real language change, not a bug fix.

I'm confused by this. The spec has three bullets here.

  1. Type parameter (does not apply)
  2. Non Nullable value type (does not apply)
  3. Otherwise....

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.

https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#1177-null-conditional-member-access

}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

comp.VerifyEmitDiagnostics();

CompileAndVerify #Closed

static unsafe void M(Test t)
{
int* p = t?.Value;
var v = t?.Value;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

var v = t?.Value;

Does this line serve any special purpose? #Closed

Copy link
Member

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
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

a?.DoSomething(); // Statement context - method call works

Is there anything special about this scenario with respect to accessing pointer types? #Closed

Copy link
Member

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;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

var g = t?.FPtr;

Isn't this covered by the previous test? #Closed

Copy link
Member

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()
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

WithReturn

It is not obvious what this suffix is supposed to convey #Closed

Copy link
Member

Choose a reason for hiding this comment

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

removed.

{
unsafe static void M1(A a)
{
byte* ptr = a?.Ptr;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 22, 2025

Choose a reason for hiding this comment

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

a?.Ptr

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

Copy link
Member

Choose a reason for hiding this comment

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

Sure 4422eb1

Copy link
Contributor

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

Copy link
Member

Choose a reason for hiding this comment

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

@copilot can you add a test similar to those added in 4422eb1 that tests that particular requested combination (Nullable and method call)

Copy link
Member

Choose a reason for hiding this comment

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

@copilot can you add a test similar to those added in 4422eb1 that tests that particular requested combination (Nullable and method call)

Copy link
Contributor Author

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)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 22, 2025

Done with review pass (commit 20) #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 22, 2025

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]>
Copy link
Contributor Author

Copilot AI commented Dec 22, 2025

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:

  • kijvsblobprodcus387.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Compilers/CSharp/Test/Semantic/Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.csproj (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@CyrusNajmabadi
Copy link
Member

Test added.

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

@CyrusNajmabadi CyrusNajmabadi merged commit 5dd606b into main Dec 23, 2025
26 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the copilot/support-null-conditional-operator branch December 23, 2025 18:16
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Dec 23, 2025
@davidwengier davidwengier modified the milestones: Next, 18.3 Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants