Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9bbba4a
Initial plan
Copilot Oct 14, 2025
7384206
Add temporary tests to understand current pointer null-conditional be…
Copilot Oct 14, 2025
807300e
Support null-conditional operator with pointer return types
Copilot Oct 14, 2025
5de452e
Add WorkItem attributes to pointer null-conditional tests
Copilot Oct 14, 2025
b5ddcc3
Update tests
CyrusNajmabadi Oct 14, 2025
f011de2
Merge branch 'copilot/support-null-conditional-operator' of https://g…
CyrusNajmabadi Oct 14, 2025
3db83b3
Merge remote-tracking branch 'upstream/main' into copilot/support-nul…
CyrusNajmabadi Dec 18, 2025
74dd89c
Allow function pointers in null-conditional expressions
Copilot Dec 18, 2025
3561f0a
Revert
CyrusNajmabadi Dec 18, 2025
d84e4de
Update comment to clarify pointer type treatment
Copilot Dec 18, 2025
43fa887
Apply suggestion from @CyrusNajmabadi
CyrusNajmabadi Dec 18, 2025
147919d
Merge remote-tracking branch 'upstream/main' into copilot/support-nul…
CyrusNajmabadi Dec 19, 2025
6f2ce9a
Update existing tests
CyrusNajmabadi Dec 19, 2025
7f3fbef
Add enhanced tests for pointer null-conditional operator
Copilot Dec 19, 2025
be4b981
Add conditional assignment tests for pointers and function pointers
Copilot Dec 19, 2025
1807832
Remove validation
CyrusNajmabadi Dec 19, 2025
3bf9f0c
Adjust test
CyrusNajmabadi Dec 19, 2025
fc1a2df
Update tests
CyrusNajmabadi Dec 19, 2025
55c2b8c
MOve 'unsafe' modifier to members from types
CyrusNajmabadi Dec 19, 2025
acb516a
UPdate test
CyrusNajmabadi Dec 19, 2025
5b81c4e
Merge remote-tracking branch 'upstream/main' into copilot/support-nul…
CyrusNajmabadi Dec 22, 2025
5112bd6
Update tests
CyrusNajmabadi Dec 22, 2025
e408a35
remove test
CyrusNajmabadi Dec 22, 2025
4422eb1
Add some tests
CyrusNajmabadi Dec 22, 2025
8dd65d8
Add test for Nullable<T> receiver with method call
Copilot Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11521,12 +11521,12 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
return GenerateBadConditionalAccessNodeError(node, receiver, access, diagnostics);
}

// 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
Contributor

@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.

Copy link
Contributor

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.

// Therefore we must reject cases resulting in types that are not reference types and cannot be lifted into nullable.
// - access cannot have unconstrained generic type
// - access cannot be a pointer
// - access cannot be a restricted type
if ((!accessType.IsReferenceType && !accessType.IsValueType) || accessType.IsPointerOrFunctionPointer() || accessType.IsRestrictedType())
// Note: Pointers (including function pointers) are allowed because they can represent null (as the zero value).
if ((!accessType.IsReferenceType && !accessType.IsValueType) || accessType.IsRestrictedType())
{
// Result type of the access is void when result value cannot be made nullable.
// For improved diagnostics we detect the cases where the value will be used and produce a
Expand All @@ -11540,10 +11540,13 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
accessType = GetSpecialType(SpecialType.System_Void, diagnostics, node);
}

// if access has value type, the type of the conditional access is nullable of that
// if access has value type (but not a pointer), the type of the conditional access is nullable of that
// https://github.com/dotnet/roslyn/issues/35075: The test `accessType.IsValueType && !accessType.IsNullableType()`
// should probably be `accessType.IsNonNullableValueType()`
if (accessType.IsValueType && !accessType.IsNullableType() && !accessType.IsVoidType())
// Note: As far as the language is concerned, pointers (including function pointers) are not value types.
// However, due to a historical quirk in the compiler implementation, we do treat them as value types.
// Since we're checking for value types here, we exclude pointers to avoid wrapping them in Nullable<>.
if (accessType.IsValueType && !accessType.IsNullableType() && !accessType.IsVoidType() && !accessType.IsPointerOrFunctionPointer())
{
accessType = GetSpecialType(SpecialType.System_Nullable_T, diagnostics, node).Construct(accessType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5616,16 +5616,18 @@ static public void F1(C c)
var compilation = CreateCompilation(source, options: TestOptions.DebugExe.WithAllowUnsafe(true));

compilation.VerifyDiagnostics(
// (16,41): error CS8977: 'void*' cannot be made nullable.
// (16,39): error CS0029: Cannot implicitly convert type 'void*' to 'object'
// Func<object, object> a = o => c?.M();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".M()").WithArguments("void*").WithLocation(16, 41),
// (19,39): error CS8977: 'void*' cannot be made nullable.
Diagnostic(ErrorCode.ERR_NoImplicitConv, "c?.M()").WithArguments("void*", "object").WithLocation(16, 39),
// (16,39): error CS1662: Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type
// Func<object, object> a = o => c?.M();
Diagnostic(ErrorCode.ERR_CantConvAnonMethReturns, "c?.M()").WithArguments("lambda expression").WithLocation(16, 39),
// (19,37): error CS0029: Cannot implicitly convert type 'void*' to 'object'
// static public object F2(C c) => c?.M();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".M()").WithArguments("void*").WithLocation(19, 39),
// (21,42): error CS8977: 'void*' cannot be made nullable.
Diagnostic(ErrorCode.ERR_NoImplicitConv, "c?.M()").WithArguments("void*", "object").WithLocation(19, 37),
// (21,32): error CS0029: Cannot implicitly convert type 'void*' to 'object'
// static public object P1 => (new C())?.M();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".M()").WithArguments("void*").WithLocation(21, 42)
);
Diagnostic(ErrorCode.ERR_NoImplicitConv, "(new C())?.M()").WithArguments("void*", "object").WithLocation(21, 32));
}

[WorkItem(1109164, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1109164")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3163,14 +3163,7 @@ void M(delegate*<void> ptr, C c)
comp.VerifyDiagnostics(
// (7,12): error CS0023: Operator '?' cannot be applied to operand of type 'delegate*<void>'
// ptr?.ToString();
Diagnostic(ErrorCode.ERR_BadUnaryOp, "?").WithArguments("?", "delegate*<void>").WithLocation(7, 12),
// (8,17): error CS8977: 'delegate*<void>' cannot be made nullable.
// ptr = c?.GetPtr();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".GetPtr()").WithArguments("delegate*<void>").WithLocation(8, 17),
// (9,12): error CS8977: 'delegate*<void>' cannot be made nullable.
// (c?.GetPtr())();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".GetPtr()").WithArguments("delegate*<void>").WithLocation(9, 12)
);
Diagnostic(ErrorCode.ERR_BadUnaryOp, "?").WithArguments("?", "delegate*<void>").WithLocation(7, 12));

var tree = comp.SyntaxTrees[0];
var model = comp.GetSemanticModel(tree);
Expand Down Expand Up @@ -3200,44 +3193,39 @@ void M(delegate*<void> ptr, C c)

FunctionPointerUtilities.VerifyFunctionPointerSemanticInfo(model, invocations[1],
expectedSyntax: "c?.GetPtr()",
expectedType: "?",
expectedType: "delegate*<System.Void>",
expectedConvertedType: "delegate*<System.Void>",
expectedSymbol: null,
expectedSymbolCandidates: null);

VerifyOperationTreeForNode(comp, model, invocations[1], expectedOperationTree: @"
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: ?, IsInvalid) (Syntax: 'c?.GetPtr()')
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: delegate*<System.Void>) (Syntax: 'c?.GetPtr()')
Operation:
IInvalidOperation (OperationKind.Invalid, Type: ?, IsImplicit) (Syntax: 'c')
Children(1):
IParameterReferenceOperation: c (OperationKind.ParameterReference, Type: C) (Syntax: 'c')
IParameterReferenceOperation: c (OperationKind.ParameterReference, Type: C) (Syntax: 'c')
WhenNotNull:
IInvocationOperation ( delegate*<System.Void> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.Void>, IsInvalid) (Syntax: '.GetPtr()')
IInvocationOperation ( delegate*<System.Void> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.Void>) (Syntax: '.GetPtr()')
Instance Receiver:
IConditionalAccessInstanceOperation (OperationKind.ConditionalAccessInstance, Type: C, IsImplicit) (Syntax: 'c')
Arguments(0)
");
Arguments(0)");

FunctionPointerUtilities.VerifyFunctionPointerSemanticInfo(model, invocations[2].Parent!.Parent!,
expectedSyntax: "(c?.GetPtr())()",
expectedType: "?",
expectedSymbol: null,
expectedType: "System.Void",
expectedSymbol: "delegate*<System.Void>",
expectedSymbolCandidates: null);

VerifyOperationTreeForNode(comp, model, invocations[2].Parent!.Parent!, expectedOperationTree: @"
IInvalidOperation (OperationKind.Invalid, Type: ?, IsInvalid) (Syntax: '(c?.GetPtr())()')
Children(1):
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: ?, IsInvalid) (Syntax: 'c?.GetPtr()')
Operation:
IInvalidOperation (OperationKind.Invalid, Type: ?, IsImplicit) (Syntax: 'c')
Children(1):
IParameterReferenceOperation: c (OperationKind.ParameterReference, Type: C) (Syntax: 'c')
WhenNotNull:
IInvocationOperation ( delegate*<System.Void> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.Void>, IsInvalid) (Syntax: '.GetPtr()')
Instance Receiver:
IConditionalAccessInstanceOperation (OperationKind.ConditionalAccessInstance, Type: C, IsImplicit) (Syntax: 'c')
Arguments(0)
");
IFunctionPointerInvocationOperation (OperationKind.FunctionPointerInvocation, Type: System.Void) (Syntax: '(c?.GetPtr())()')
Target:
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: delegate*<System.Void>) (Syntax: 'c?.GetPtr()')
Operation:
IParameterReferenceOperation: c (OperationKind.ParameterReference, Type: C) (Syntax: 'c')
WhenNotNull:
IInvocationOperation ( delegate*<System.Void> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.Void>) (Syntax: '.GetPtr()')
Instance Receiver:
IConditionalAccessInstanceOperation (OperationKind.ConditionalAccessInstance, Type: C, IsImplicit) (Syntax: 'c')
Arguments(0)
Arguments(0)");

}

Expand Down Expand Up @@ -3299,37 +3287,35 @@ .maxstack 2

FunctionPointerUtilities.VerifyFunctionPointerSemanticInfo(model, invocations[0],
expectedSyntax: "c?.GetPtr()",
expectedType: "System.Void",
expectedType: "delegate*<System.String>",
expectedSymbol: null,
expectedSymbolCandidates: null);

VerifyOperationTreeForNode(comp, model, invocations[0], expectedOperationTree: @"
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: System.Void) (Syntax: 'c?.GetPtr()')
Operation:
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: delegate*<System.String>) (Syntax: 'c?.GetPtr()')
Operation:
ILocalReferenceOperation: c (OperationKind.LocalReference, Type: C) (Syntax: 'c')
WhenNotNull:
WhenNotNull:
IInvocationOperation ( delegate*<System.String> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.String>) (Syntax: '.GetPtr()')
Instance Receiver:
Instance Receiver:
IConditionalAccessInstanceOperation (OperationKind.ConditionalAccessInstance, Type: C, IsImplicit) (Syntax: 'c')
Arguments(0)
");
Arguments(0)");

FunctionPointerUtilities.VerifyFunctionPointerSemanticInfo(model, invocations[1],
expectedSyntax: "c?.GetPtr()",
expectedType: "System.Void",
expectedType: "delegate*<System.String>",
expectedSymbol: null,
expectedSymbolCandidates: null);

VerifyOperationTreeForNode(comp, model, invocations[1], expectedOperationTree: @"
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: System.Void) (Syntax: 'c?.GetPtr()')
Operation:
IConditionalAccessOperation (OperationKind.ConditionalAccess, Type: delegate*<System.String>) (Syntax: 'c?.GetPtr()')
Operation:
ILocalReferenceOperation: c (OperationKind.LocalReference, Type: C) (Syntax: 'c')
WhenNotNull:
WhenNotNull:
IInvocationOperation ( delegate*<System.String> C.GetPtr()) (OperationKind.Invocation, Type: delegate*<System.String>) (Syntax: '.GetPtr()')
Instance Receiver:
Instance Receiver:
IConditionalAccessInstanceOperation (OperationKind.ConditionalAccessInstance, Type: C, IsImplicit) (Syntax: 'c')
Arguments(0)
");
Arguments(0)");
}

[Fact]
Expand Down
Loading
Loading