diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index e908cec91fcf9..110765ccfe7ab 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -2784,6 +2784,7 @@ internal static uint GetValEscape(BoundExpression expr, uint scopeOfTheContainin case BoundKind.AsOperator: case BoundKind.AwaitExpression: case BoundKind.ConditionalAccess: + case BoundKind.ConditionalReceiver: case BoundKind.ArrayAccess: // only possible in error cases (if possible at all) return scopeOfTheContainingExpression; diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedExplicitConversions.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedExplicitConversions.cs index 63aa33c7cd866..9c87d16fc8eda 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedExplicitConversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedExplicitConversions.cs @@ -265,10 +265,10 @@ private void AddUserDefinedConversionsToExplicitCandidateSet( if (fromConversion.Exists && toConversion.Exists) { - if ((object)source != null && source.IsNullableType() && convertsFrom.IsNonNullableValueType() && target.CanBeAssignedNull()) + if ((object)source != null && source.IsNullableType() && convertsFrom.IsValidNullableTypeArgument() && target.CanBeAssignedNull()) { TypeSymbol nullableFrom = MakeNullableType(convertsFrom); - TypeSymbol nullableTo = convertsTo.IsNonNullableValueType() ? MakeNullableType(convertsTo) : convertsTo; + TypeSymbol nullableTo = convertsTo.IsValidNullableTypeArgument() ? MakeNullableType(convertsTo) : convertsTo; Conversion liftedFromConversion = EncompassingExplicitConversion(sourceExpression, source, nullableFrom, ref useSiteInfo); Conversion liftedToConversion = EncompassingExplicitConversion(null, nullableTo, target, ref useSiteInfo); Debug.Assert(liftedFromConversion.Exists); @@ -290,15 +290,16 @@ private void AddUserDefinedConversionsToExplicitCandidateSet( // though it really were X?-->Y for the purposes of determining the best // source type of a set of operators. // - // We perpetuate these fictions here. + // We perpetuate these fictions here, except when X or Y is not a valid + // type argument to `Nullable`. - if (target.IsNullableType() && convertsTo.IsNonNullableValueType()) + if (target.IsNullableType() && convertsTo.IsValidNullableTypeArgument()) { convertsTo = MakeNullableType(convertsTo); toConversion = EncompassingExplicitConversion(null, convertsTo, target, ref useSiteInfo); } - if ((object)source != null && source.IsNullableType() && convertsFrom.IsNonNullableValueType()) + if ((object)source != null && source.IsNullableType() && convertsFrom.IsValidNullableTypeArgument()) { convertsFrom = MakeNullableType(convertsFrom); fromConversion = EncompassingExplicitConversion(null, convertsFrom, source, ref useSiteInfo); diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs index 03b3cffe5c1b0..a1e447450d3af 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs @@ -304,9 +304,12 @@ void addCandidatesFromType( // actually X-->Y? in source for the purposes of determining the best target // type of an operator. // - // We perpetuate this fiction here. + // We perpetuate this fiction here, except for cases when Y is not a valid type + // argument for Nullable. This scenario should only be possible when the corlib + // defines a type such as int or long to be a ref struct (see + // LiftedConversion_InvalidTypeArgument02). - if ((object)target != null && target.IsNullableType() && convertsTo.IsNonNullableValueType()) + if ((object)target != null && target.IsNullableType() && convertsTo.IsValidNullableTypeArgument()) { convertsTo = MakeNullableType(convertsTo); toConversion = allowAnyTarget ? Conversion.Identity : @@ -315,7 +318,7 @@ void addCandidatesFromType( u.Add(UserDefinedConversionAnalysis.Normal(constrainedToTypeOpt, op, fromConversion, toConversion, convertsFrom, convertsTo)); } - else if ((object)source != null && source.IsNullableType() && convertsFrom.IsNonNullableValueType() && + else if ((object)source != null && source.IsNullableType() && convertsFrom.IsValidNullableTypeArgument() && (allowAnyTarget || target.CanBeAssignedNull())) { // As mentioned above, here we diverge from the specification, in two ways. @@ -334,7 +337,7 @@ void addCandidatesFromType( // If the answer to all those questions is "yes" then we lift to nullable // and see if the resulting operator is applicable. TypeSymbol nullableFrom = MakeNullableType(convertsFrom); - TypeSymbol nullableTo = convertsTo.IsNonNullableValueType() ? MakeNullableType(convertsTo) : convertsTo; + TypeSymbol nullableTo = convertsTo.IsValidNullableTypeArgument() ? MakeNullableType(convertsTo) : convertsTo; Conversion liftedFromConversion = EncompassingImplicitConversion(sourceExpression, source, nullableFrom, ref useSiteInfo); Conversion liftedToConversion = !allowAnyTarget ? EncompassingImplicitConversion(null, nullableTo, target, ref useSiteInfo) : diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorOverloadResolution.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorOverloadResolution.cs index 77c94f0db82a2..82fe0fb80460a 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorOverloadResolution.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorOverloadResolution.cs @@ -929,10 +929,8 @@ private static LiftingResult UserDefinedBinaryOperatorCanBeLifted(TypeSymbol lef // SPEC: types and if the result type is bool. The lifted form is // SPEC: constructed by adding a single ? modifier to each operand type. - if (!left.IsValueType || - left.IsNullableType() || - !right.IsValueType || - right.IsNullableType()) + if (!left.IsValidNullableTypeArgument() || + !right.IsValidNullableTypeArgument()) { return LiftingResult.NotLifted; } @@ -953,7 +951,7 @@ private static LiftingResult UserDefinedBinaryOperatorCanBeLifted(TypeSymbol lef LiftingResult.LiftOperandsButNotResult : LiftingResult.NotLifted; default: - return result.IsValueType && !result.IsNullableType() ? + return result.IsValidNullableTypeArgument() ? LiftingResult.LiftOperandsAndResult : LiftingResult.NotLifted; } diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/UnaryOperatorOverloadResolution.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/UnaryOperatorOverloadResolution.cs index 68f3de987afe9..d0078360916e2 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/UnaryOperatorOverloadResolution.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Operators/UnaryOperatorOverloadResolution.cs @@ -500,8 +500,8 @@ private void GetUserDefinedUnaryOperatorsFromType( case UnaryOperatorKind.PostfixIncrement: case UnaryOperatorKind.LogicalNegation: case UnaryOperatorKind.BitwiseComplement: - if (operandType.IsValueType && !operandType.IsNullableType() && - resultType.IsValueType && !resultType.IsNullableType()) + if (operandType.IsValidNullableTypeArgument() && + resultType.IsValidNullableTypeArgument()) { operators.Add(new UnaryOperatorSignature( UnaryOperatorKind.Lifted | UnaryOperatorKind.UserDefined | kind, diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs index a16de1ab9dc66..533277236b35e 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs @@ -1052,7 +1052,7 @@ private BoundExpression RewriteUserDefinedConversion( private BoundExpression MakeLiftedUserDefinedConversionConsequence(BoundCall call, TypeSymbol resultType) { - if (call.Method.ReturnType.IsNonNullableValueType()) + if (call.Method.ReturnType.IsValidNullableTypeArgument()) { Debug.Assert(resultType.IsNullableType() && TypeSymbol.Equals(resultType.GetNullableUnderlyingType(), call.Method.ReturnType, TypeCompareKind.ConsiderEverything2)); MethodSymbol ctor = UnsafeGetNullableMethod(call.Syntax, resultType, SpecialMember.System_Nullable_T__ctor); diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs index 3272323630c12..6b0f2459f47c4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs @@ -132,6 +132,14 @@ public static bool IsNullableType(this TypeSymbol type) return type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; } + public static bool IsValidNullableTypeArgument(this TypeSymbol type) + { + return type is { IsValueType: true } + && !type.IsNullableType() + && !type.IsPointerOrFunctionPointer() + && !type.IsRestrictedType(); + } + public static TypeSymbol GetNullableUnderlyingType(this TypeSymbol type) { return type.GetNullableUnderlyingTypeWithAnnotations().Type; diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/OperatorTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/OperatorTests.cs index d4676cf594cf0..a65f2332c223d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/OperatorTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/OperatorTests.cs @@ -11214,5 +11214,119 @@ static unsafe void M(dynamic d, int* p) Diagnostic(ErrorCode.ERR_BadBinaryOps, "d += p").WithArguments("+=", "dynamic", "int*").WithLocation(5, 9) ); } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedUnaryOperator_InvalidTypeArgument01() + { + var code = @" +S1? s1 = default; +var s2 = +s1; + +struct S1 +{ + public static S2 operator+(S1 s1) => throw null; +} + +ref struct S2 {} +"; + + var comp = CreateCompilation(code); + comp.VerifyDiagnostics( + // (3,10): error CS0023: Operator '+' cannot be applied to operand of type 'S1?' + // var s2 = +s1; + Diagnostic(ErrorCode.ERR_BadUnaryOp, "+s1").WithArguments("+", "S1?").WithLocation(3, 10) + ); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedUnaryOperator_InvalidTypeArgument02() + { + var code = @" +S1? s1 = default; +var s2 = +s1; + +unsafe struct S1 +{ + public static unsafe int* operator+(S1 s1) => throw null; +} +"; + + var comp = CreateCompilation(code, options: TestOptions.UnsafeReleaseExe); + comp.VerifyDiagnostics( + // (3,10): error CS0023: Operator '+' cannot be applied to operand of type 'S1?' + // var s2 = +s1; + Diagnostic(ErrorCode.ERR_BadUnaryOp, "+s1").WithArguments("+", "S1?").WithLocation(3, 10) + ); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedBinaryOperator_InvalidTypeArgument01() + { + var code = @" +var x = new S1(); +int? y = 1; +(x + y)?.M(); + +public readonly ref struct S1 +{ + public static S1 operator+ (S1 x, int y) => throw null; + public void M() {} +} +"; + + var comp = CreateCompilation(code); + comp.VerifyDiagnostics( + // (4,2): error CS0019: Operator '+' cannot be applied to operands of type 'S1' and 'int?' + // (x + y)?.M(); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "x + y").WithArguments("+", "S1", "int?").WithLocation(4, 2) + ); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedBinaryOperator_InvalidTypeArgument02() + { + var code = @" +var x = new S1(); +int? y = 1; +(y + x)?.M(); + +public readonly ref struct S1 +{ + public static S1 operator+ (int y, S1 x) => throw null; + public void M() {} +} +"; + + var comp = CreateCompilation(code); + comp.VerifyDiagnostics( + // (4,2): error CS0019: Operator '+' cannot be applied to operands of type 'int?' and 'S1' + // (y + x)?.M(); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "y + x").WithArguments("+", "int?", "S1").WithLocation(4, 2) + ); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedBinaryOperator_InvalidTypeArgument03() + { + var code = @" +var x = new S1(); +int? y = 1; +(y > x).ToString(); + +public readonly ref struct S1 +{ + public static bool operator >(int y, S1 x) => throw null; + public static bool operator <(int y, S1 x) => throw null; + public void M() {} +} +"; + + var comp = CreateCompilation(code); + comp.VerifyDiagnostics( + // (4,2): error CS0019: Operator '>' cannot be applied to operands of type 'int?' and 'S1' + // (y > x).ToString(); + Diagnostic(ErrorCode.ERR_BadBinaryOps, "y > x").WithArguments(">", "int?", "S1").WithLocation(4, 2) + ); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs index b7cb6641e57b9..b97f670c35d9b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs @@ -1647,5 +1647,266 @@ private static C M2() // return (C) M1(); Diagnostic(ErrorCode.ERR_NoExplicitConv, "(C) M1()").WithArguments("void", "C").WithLocation(9, 16)); } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument01() + { + var code = @" +int? i = null; +C c = i; + +class C +{ + public static implicit operator C(long l) => throw null; +} + +namespace System +{ + public class Object {} + public class String {} + public class Exception {} + public class ValueType : Object {} + public struct Void {} + public struct Int32 {} + public struct Nullable where T : struct {} + public ref struct Int64 {} +} +"; + + var comp = CreateEmptyCompilation(code); + comp.VerifyDiagnostics( + // (3,7): error CS0266: Cannot implicitly convert type 'int?' to 'C'. An explicit conversion exists (are you missing a cast?) + // C c = i; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "i").WithArguments("int?", "C").WithLocation(3, 7) + ); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument02() + { + var code = @" +C c = null; +M(c); + +void M(long? i) => throw null; + +class C +{ + public static implicit operator int(C c) => throw null; +} + +namespace System +{ + public class Object {} + public class String {} + public class Exception {} + public class ValueType : Object {} + public struct Void {} + public ref struct Int32 {} + public struct Nullable where T : struct { public Nullable(T value) {} } + public struct Int64 {} + public class Attribute {} +} +"; + + var comp = CreateEmptyCompilation(code); + var verifier = CompileAndVerify(comp, verify: Verification.Skipped); + + // Note that no int? is being created. + verifier.VerifyIL("", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldnull + IL_0001: call ""int C.op_Implicit(C)"" + IL_0006: conv.i8 + IL_0007: newobj ""long?..ctor(long)"" + IL_000c: call ""void Program.<
$>g__M|0_0(long?)"" + IL_0011: ret +} +"); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument03() + { + var code = @" +int? i = null; +C c = (C)i; + +class C +{ + public static explicit operator C(long l) => throw null; +} + +namespace System +{ + public class Object {} + public class String {} + public class Exception {} + public class ValueType : Object {} + public struct Void {} + public struct Int32 {} + public struct Nullable where T : struct { public T Value { get => throw null; } } + public ref struct Int64 {} + public class Attribute {} +} +"; + + var comp = CreateEmptyCompilation(code); + var verifier = CompileAndVerify(comp, verify: Verification.Skipped); + + verifier.VerifyIL("", @" +{ + // Code size 23 (0x17) + .maxstack 1 + .locals init (int? V_0) //i + IL_0000: ldloca.s V_0 + IL_0002: initobj ""int?"" + IL_0008: ldloca.s V_0 + IL_000a: call ""int int?.Value.get"" + IL_000f: conv.i8 + IL_0010: call ""C C.op_Explicit(long)"" + IL_0015: pop + IL_0016: ret +} +"); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument04() + { + var code = @" +C c = null; +M((long?)c); + +void M(long? i) => throw null; + +class C +{ + public static explicit operator int(C c) => throw null; +} + +namespace System +{ + public class Object {} + public class String {} + public class Exception {} + public class ValueType : Object {} + public struct Void {} + public ref struct Int32 {} + public struct Nullable where T : struct { public Nullable(T value) {} } + public struct Int64 {} + public class Attribute {} +} +"; + + var comp = CreateEmptyCompilation(code); + var verifier = CompileAndVerify(comp, verify: Verification.Skipped); + + // Note that no int? is being created. + verifier.VerifyIL("", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldnull + IL_0001: call ""int C.op_Explicit(C)"" + IL_0006: conv.i8 + IL_0007: newobj ""long?..ctor(long)"" + IL_000c: call ""void Program.<
$>g__M|0_0(long?)"" + IL_0011: ret +} +"); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument05() + { + var code = @" +unsafe +{ + S? s = null; + void* f = s; + System.Console.WriteLine((int)f); +} + +public struct S +{ + public static unsafe implicit operator void*(S v) => throw null; +} +"; + + var comp = CreateCompilation(code, options: TestOptions.UnsafeReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "0", verify: Verification.Skipped); + + verifier.VerifyIL("", @" +{ + // Code size 42 (0x2a) + .maxstack 1 + .locals init (S? V_0) + IL_0000: ldloca.s V_0 + IL_0002: initobj ""S?"" + IL_0008: ldloc.0 + IL_0009: stloc.0 + IL_000a: ldloca.s V_0 + IL_000c: call ""bool S?.HasValue.get"" + IL_0011: brtrue.s IL_0017 + IL_0013: ldc.i4.0 + IL_0014: conv.u + IL_0015: br.s IL_0023 + IL_0017: ldloca.s V_0 + IL_0019: call ""S S?.GetValueOrDefault()"" + IL_001e: call ""void* S.op_Implicit(S)"" + IL_0023: conv.i4 + IL_0024: call ""void System.Console.WriteLine(int)"" + IL_0029: ret +} +"); + } + + [Fact, WorkItem(56646, "https://github.com/dotnet/roslyn/issues/56646")] + public void LiftedConversion_InvalidTypeArgument06() + { + var code = @" +unsafe +{ + S? s = null; + void* f = (void*)s; + System.Console.WriteLine((int)f); +} + +public struct S +{ + public static unsafe explicit operator void*(S v) => throw null; +} +"; + + var comp = CreateCompilation(code, options: TestOptions.UnsafeReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "0", verify: Verification.Skipped); + + verifier.VerifyIL("", @" +{ + // Code size 42 (0x2a) + .maxstack 1 + .locals init (S? V_0) + IL_0000: ldloca.s V_0 + IL_0002: initobj ""S?"" + IL_0008: ldloc.0 + IL_0009: stloc.0 + IL_000a: ldloca.s V_0 + IL_000c: call ""bool S?.HasValue.get"" + IL_0011: brtrue.s IL_0017 + IL_0013: ldc.i4.0 + IL_0014: conv.u + IL_0015: br.s IL_0023 + IL_0017: ldloca.s V_0 + IL_0019: call ""S S?.GetValueOrDefault()"" + IL_001e: call ""void* S.op_Explicit(S)"" + IL_0023: conv.i4 + IL_0024: call ""void System.Console.WriteLine(int)"" + IL_0029: ret +} +"); + } } }