Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 8 additions & 7 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11387,17 +11387,17 @@ 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
Member

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.

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.

// 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 function pointer
// - 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())
{
// 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
// more specific (though not technically correct) diagnostic here:
// "Error CS0023: Operator '?' cannot be applied to operand of type 'T'"
// more specific error here.
if (ResultIsUsed(node))
{
return GenerateBadConditionalAccessNodeError(node, receiver, access, diagnostics);
Expand All @@ -11406,10 +11406,11 @@ 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: Pointers are not wrapped in Nullable<> because they can already represent null (as the zero value).
if (accessType.IsValueType && !accessType.IsNullableType() && !accessType.IsVoidType() && !accessType.IsPointerType())
{
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 @@ -2892,5 +2892,167 @@ static void M(bool b, C? c1, C? c2)
""",
graph, symbol);
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_Simple()
{
var source = """
public unsafe class A
{
public byte* Ptr = null;
}

unsafe class Test
{
static void M1(A a)
{
byte* ptr = a?.Ptr;
}

static void M2(A a)
{
var result = a?.Ptr;
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_WithUsage()
{
var source = """
using System;

public unsafe class A
{
public byte* Ptr = null;
}

unsafe class Test
{
static void Main()
{
var a = new A();
byte* ptr1 = a?.Ptr;
Console.Write(ptr1 == null ? "null" : "not null");

a = null;
byte* ptr2 = a?.Ptr;
Console.Write(ptr2 == null ? " null" : " not null");
}
}
""";
var verifier = CompileAndVerify(source, options: TestOptions.UnsafeDebugExe, verify: Verification.Skipped, expectedOutput: "null null");
verifier.VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_IntPointer()
{
var source = """
unsafe class Test
{
public int* Value = null;

static void M(Test t)
{
int* p = t?.Value;
var v = t?.Value;
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_VoidPointer()
{
var source = """
unsafe class Test
{
public void* Data = null;

static void M(Test t)
{
void* p = t?.Data;
var d = t?.Data;
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_Chained()
{
var source = """
unsafe struct Node
{
public Node* Next;
public int Value;

public Node(int v) { Next = null; Value = v; }
}

unsafe class Test
{
public Node* Head = null;

static void M(Test t)
{
Node* n1 = t?.Head;
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void PointerReturnType_StatementContext()
{
var source = """
public unsafe class A
{
public byte* Ptr = null;
public void DoSomething() { }
}

unsafe class Test
{
static void M(A a)
{
a?.DoSomething(); // Statement context - method call works
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7502")]
public void FunctionPointerReturnType_StillRejected()
{
var source = """
unsafe class Test
{
public delegate*<int, void> FPtr = null;

static void M(Test t)
{
var f = t?.FPtr;
}
}
""";
var comp = CreateCompilation(source, options: TestOptions.UnsafeDebugDll);
comp.VerifyEmitDiagnostics(
// (7,19): error CS8978: 'delegate*<int, void>' cannot be made nullable.
// var f = t?.FPtr;
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".FPtr").WithArguments("delegate*<int, void>").WithLocation(7, 19));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24354,11 +24354,28 @@ unsafe static void Main()
}

";
CreateCompilationWithMscorlib461(text, options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics(
// (9,24): error CS8977: 'void*' cannot be made nullable.
// var p = intPtr?.ToPointer();
Diagnostic(ErrorCode.ERR_CannotBeMadeNullable, ".ToPointer()").WithArguments("void*").WithLocation(9, 24)
);
CompileAndVerify(text, options: TestOptions.UnsafeReleaseDll)
.VerifyDiagnostics()
.VerifyIL("Program.Main", """
{
// Code size 34 (0x22)
.maxstack 1
.locals init (System.IntPtr? V_0, //intPtr
System.IntPtr V_1)
IL_0000: ldloca.s V_0
IL_0002: initobj "System.IntPtr?"
IL_0008: ldloca.s V_0
IL_000a: call "bool System.IntPtr?.HasValue.get"
IL_000f: brfalse.s IL_0021
IL_0011: ldloca.s V_0
IL_0013: call "System.IntPtr System.IntPtr?.GetValueOrDefault()"
IL_0018: stloc.1
IL_0019: ldloca.s V_1
IL_001b: call "void* System.IntPtr.ToPointer()"
IL_0020: pop
IL_0021: ret
}
""");
}

[Fact]
Expand Down
Loading