// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.CSharp.UnitTests.Emit; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen { public partial class CodeGenOptimizedNullableOperatorTests : CSharpTestBase { [Fact] public void TestNullableBoxingConversionsAlwaysNull() { // The native compiler does not optimize this case; Roslyn does. We know // that the result of boxing default(int?) to object is the same as casting // literal null to object, so we do not need to allocate space on the stack // for the nullable int, initialize it, and then box that to a null ref. string[] sources = { @"class Program { static void Main() { System.Console.WriteLine((object)default(int?)); } } ", @"class Program { static void Main() { System.Console.WriteLine((object)(new int?())); } } ", @"class Program { static void Main() { System.Console.WriteLine((object)(int?)null); } } "}; string expectedOutput = ""; string expectedIL = @"{ // Code size 7 (0x7) .maxstack 1 IL_0000: ldnull IL_0001: call ""void System.Console.WriteLine(object)"" IL_0006: ret }"; foreach (string source in sources) { var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.Main", expectedIL); } } [Fact] public void TestNullableBoxingConversionNeverNull() { // The native compiler does not optimize this case; Roslyn does. We know // that the result of boxing default(int?) to object is the same as casting // literal null to object, so we do not need to allocate space on the stack // for the nullable int, initialize it, and then box that to a null ref. string[] sources = { @"class Program { static void Main() { System.Console.WriteLine((object)new int?(123)); } } ", @"class Program { static void Main() { System.Console.WriteLine((object)(int?)123); } } "}; string expectedOutput = "123"; string expectedIL = @"{ // Code size 13 (0xd) .maxstack 1 IL_0000: ldc.i4.s 123 IL_0002: box ""int"" IL_0007: call ""void System.Console.WriteLine(object)"" IL_000c: ret }"; foreach (string source in sources) { var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.Main", expectedIL); } } [Fact] public void TestNullableConversionAlwaysNull() { // A built-in nullable conversion whose argument is known to always be null // can simply be optimized away to be the null result. string source = @" class Program { static long? M() { return new int?(); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (long? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""long?"" IL_0008: ldloc.0 IL_0009: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestNullableConversionNeverNull() { // A built-in nullable conversion whose argument is known to be non-null // can be generated by converting the value to the underlying target type, // and then converting that to nullable, without generating the nullable source // or checking to see if it has a value. string source = @" class Program { static long? M(int x) { return new int?(x); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: ldarg.0 IL_0001: conv.i8 IL_0002: newobj ""long?..ctor(long)"" IL_0007: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestLiftedUserDefinedConversionAlwaysNull() { // A user-defined nullable conversion whose argument is known to always be null // can simply be optimized away to be the null result. string source = @" struct S { public static implicit operator S(int x) { return new S(); } } class Program { static S? M() { return new int?(); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (S? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""S?"" IL_0008: ldloc.0 IL_0009: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestLiftedUserDefinedConversionNeverNull() { // A user-defined nullable conversion whose argument is known to never be null // can have the nullable ctor, temporary store and value test optimized away. string source = @" struct S { public static implicit operator S(int x) { return new S(); } public static implicit operator string(S s) { return s.ToString(); } } class Program { static S? M1(int x) { return new int?(x); } static S M2(int x) { return (S)(new int?(x)); } static string M3(int x) { // The non-null conversion optimizer should chain well. That is, // we first optimize (string)(S?)(new int?(x)) to (string)(new S?((S)x)), and then // to (string)(S)x. return (string)(S?)(new int?(x)); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 12 (0xc) .maxstack 1 IL_0000: ldarg.0 IL_0001: call ""S S.op_Implicit(int)"" IL_0006: newobj ""S?..ctor(S)"" IL_000b: ret }"; string expectedIL2 = @"{ // Code size 7 (0x7) .maxstack 1 IL_0000: ldarg.0 IL_0001: call ""S S.op_Implicit(int)"" IL_0006: ret }"; string expectedIL3 = @"{ // Code size 12 (0xc) .maxstack 1 IL_0000: ldarg.0 IL_0001: call ""S S.op_Implicit(int)"" IL_0006: call ""string S.op_Implicit(S)"" IL_000b: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); } [Fact] public void TestNullableUnaryOpsAlwaysNull() { // A unary operator whose argument is known to always be null // can simply be optimized away to be the null result. string source = @" class Program { static int? M() { return ~(new int?()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (int? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""int?"" IL_0008: ldloc.0 IL_0009: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (6,16): warning CS0458: The result of the expression is always 'null' of type 'int?' // return ~(new int?()); Diagnostic(ErrorCode.WRN_AlwaysNull, "~(new int?())").WithArguments("int?")); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestNullableUnaryOpsAlwaysNullChained() { // A unary operator whose argument is known to always be null // can simply be optimized away to be the null result. These // optimizations should "chain" naturally. Here we combine // a built-in conversion, two unary operations, and a boxing. // The net result should simply be a null reference. // // The native compiler does not handle these "chained" optimizations, // interestingly enough; the native compiler will optimize away only the // innermost one; it is then not treated as "always null" and is checked // for nullity unnecessarily. // // Fortunately, the warning logic does not do a deep analysis; it only // reports a single warning. string source = @" class Program { static object M() { return ~-(new short?()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 2 (0x2) .maxstack 1 IL_0000: ldnull IL_0001: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (6,17): warning CS0458: The result of the expression is always 'null' of type 'int?' // return ~-(new short?()); Diagnostic(ErrorCode.WRN_AlwaysNull, "-(new short?())").WithArguments("int?")); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestNullableUnaryOpsNeverNull() { // A unary operator whose argument is known to never be null // can be optimized to avoid the null check. string source = @" class Program { static int N() { return 123; } static int? M() { // This can be optimized to new int?(~N()) return ~(new int?(N())); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 12 (0xc) .maxstack 1 IL_0000: call ""int Program.N()"" IL_0005: not IL_0006: newobj ""int?..ctor(int)"" IL_000b: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestNullableUnaryOpsNeverNullChained() { // A unary operator whose argument is known to never be null // can simply be optimized away to be the null result. These // optimizations should "chain" naturally. Here we combine // two unary operations and a boxing. As you can see, we eliminate // all the null checks and the "new S?" ctor. string source = @" struct S { public static S operator +(S s) { return s; } public static S operator -(S s) { return s; } public static S operator ~(S s) { return s; } } class Program { static object M(S s) { return ~-(new S?(s)); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 17 (0x11) .maxstack 1 IL_0000: ldarg.0 IL_0001: call ""S S.op_UnaryNegation(S)"" IL_0006: call ""S S.op_OnesComplement(S)"" IL_000b: box ""S"" IL_0010: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestLiftedUnaryOpOnTopOfLifted() { // Here's an optimization that the dev10 compiler does not do. If we have a // lifted unary operator "on top" of another lifted operation, then the unary // operation can be "distributed" to both branches of the underlying lifted operation. // // For example, suppose we have // // return -(N1() + N2()); // // Where N1() and N2() return int?. The dev10 compiler does this in two steps: first it // computes the int? result of the addition, and then it does a fully lifted negation: // // int? t1 = N1(); // int? t2 = N2(); // int? t3 = t1.HasValue && t2.HasValue ? new int?(t1.Value + t2.Value) : new int?(); // return t3.HasValue ? new int?(-t3.Value)) : new int?(); // // But t3 is completely unnecessary here. We could realize this as: // // return -(t1.HasValue && t2.HasValue ? new int?(t1.Value + t2.Value) : new int?()) // // which is the same as distributing the conversion to the consequence and alternative: // // return (t1.HasValue && t2.HasValue ? - new int?(t1.Value + t2.Value) ): - ( new int?() ) ) // // and now we can optimize the consequence and alternative down to // // return (t1.HasValue && t2.HasValue ? new int?(-(t1.Value + t2.Value) ): new int?() ) // // And the int? t3 disappears entirely. // // This optimization has the nice property that it composes well with itself, as we'll see. string source = @" struct S { public static S operator -(S s) { return s; } public static S operator ~(S s) { return s; } public static S operator +(S s1, S s2) { return s1; } } class Program { static int? N1() { return 1; } static int? N2() { return 2; } static S? N3() { return null; } static S? N4() { return null; } static int? M1() { return -(N1() + N2()); } static S? M2() { return -~(N3() + N4()); } static void Main() { } } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 61 (0x3d) .maxstack 2 .locals init (int? V_0, int? V_1, int? V_2) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.0 IL_0006: call ""int? Program.N2()"" IL_000b: stloc.1 IL_000c: ldloca.s V_0 IL_000e: call ""bool int?.HasValue.get"" IL_0013: ldloca.s V_1 IL_0015: call ""bool int?.HasValue.get"" IL_001a: and IL_001b: brtrue.s IL_0027 IL_001d: ldloca.s V_2 IL_001f: initobj ""int?"" IL_0025: ldloc.2 IL_0026: ret IL_0027: ldloca.s V_0 IL_0029: call ""int int?.GetValueOrDefault()"" IL_002e: ldloca.s V_1 IL_0030: call ""int int?.GetValueOrDefault()"" IL_0035: add IL_0036: neg IL_0037: newobj ""int?..ctor(int)"" IL_003c: ret } "; string expectedIL2 = @"{ // Code size 74 (0x4a) .maxstack 2 .locals init (S? V_0, S? V_1, S? V_2) IL_0000: call ""S? Program.N3()"" IL_0005: stloc.0 IL_0006: call ""S? Program.N4()"" IL_000b: stloc.1 IL_000c: ldloca.s V_0 IL_000e: call ""bool S?.HasValue.get"" IL_0013: ldloca.s V_1 IL_0015: call ""bool S?.HasValue.get"" IL_001a: and IL_001b: brtrue.s IL_0027 IL_001d: ldloca.s V_2 IL_001f: initobj ""S?"" IL_0025: ldloc.2 IL_0026: ret IL_0027: ldloca.s V_0 IL_0029: call ""S S?.GetValueOrDefault()"" IL_002e: ldloca.s V_1 IL_0030: call ""S S?.GetValueOrDefault()"" IL_0035: call ""S S.op_Addition(S, S)"" IL_003a: call ""S S.op_OnesComplement(S)"" IL_003f: call ""S S.op_UnaryNegation(S)"" IL_0044: newobj ""S?..ctor(S)"" IL_0049: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); } [Fact] public void TestLiftedBinaryOpWithConstantOnTopOfLifted() { // If we have a lifted binary operation "on top" of a lifted operation, // and the right side of the outer operation is a constant, then we // can eliminate several temporaries. // // For example, suppose we have // // return N1() * N2() + 1; // // Where N1() and N2() return int?. We could do this the obvious way: // // int? n1 = N1(); // int? n2 = N2(); // int? r = n1.HasValue && n2.HasValue ? new int?(n1.Value * n2.Value) : new int?(); // int v = 1; // return r.HasValue ? new int?(r.Value + v)) : new int?(); // // But r and v are both unnecessary. We could instead realize this as: // // int? n1 = N1(); // int? n2 = N2(); // return n1.HasValue && n2.HasValue ? new int?(n1.Value * n2.Value + 1) : new int?(); // // We want to do this optimization in particular because it makes codegen for i++ // and i+=1 better. // // The dev10 compiler does this optimization *only* on i++ and not on expressions // like N1() * N2() + 1 or sh+=1; string source = @" class Program { static int? N1() { return 1; } static int? N2() { return 2; } static short? sh; static int? M1() { return N1() * N2() + 1; } static short? M2() { return sh++; } static short? M3() { return ++sh; } static short? M4() { return sh += 1; } static void Main() { } } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 62 (0x3e) .maxstack 2 .locals init (int? V_0, int? V_1, int? V_2) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.0 IL_0006: call ""int? Program.N2()"" IL_000b: stloc.1 IL_000c: ldloca.s V_0 IL_000e: call ""bool int?.HasValue.get"" IL_0013: ldloca.s V_1 IL_0015: call ""bool int?.HasValue.get"" IL_001a: and IL_001b: brtrue.s IL_0027 IL_001d: ldloca.s V_2 IL_001f: initobj ""int?"" IL_0025: ldloc.2 IL_0026: ret IL_0027: ldloca.s V_0 IL_0029: call ""int int?.GetValueOrDefault()"" IL_002e: ldloca.s V_1 IL_0030: call ""int int?.GetValueOrDefault()"" IL_0035: mul IL_0036: ldc.i4.1 IL_0037: add IL_0038: newobj ""int?..ctor(int)"" IL_003d: ret }"; string expectedIL2 = @"{ // Code size 48 (0x30) .maxstack 3 .locals init (short? V_0, short? V_1) IL_0000: ldsfld ""short? Program.sh"" IL_0005: dup IL_0006: stloc.0 IL_0007: ldloca.s V_0 IL_0009: call ""bool short?.HasValue.get"" IL_000e: brtrue.s IL_001b IL_0010: ldloca.s V_1 IL_0012: initobj ""short?"" IL_0018: ldloc.1 IL_0019: br.s IL_002a IL_001b: ldloca.s V_0 IL_001d: call ""short short?.GetValueOrDefault()"" IL_0022: ldc.i4.1 IL_0023: add IL_0024: conv.i2 IL_0025: newobj ""short?..ctor(short)"" IL_002a: stsfld ""short? Program.sh"" IL_002f: ret }"; string expectedIL3 = @"{ // Code size 48 (0x30) .maxstack 2 .locals init (short? V_0, short? V_1) IL_0000: ldsfld ""short? Program.sh"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool short?.HasValue.get"" IL_000d: brtrue.s IL_001a IL_000f: ldloca.s V_1 IL_0011: initobj ""short?"" IL_0017: ldloc.1 IL_0018: br.s IL_0029 IL_001a: ldloca.s V_0 IL_001c: call ""short short?.GetValueOrDefault()"" IL_0021: ldc.i4.1 IL_0022: add IL_0023: conv.i2 IL_0024: newobj ""short?..ctor(short)"" IL_0029: dup IL_002a: stsfld ""short? Program.sh"" IL_002f: ret }"; string expectedIL4 = @"{ // Code size 48 (0x30) .maxstack 2 .locals init (short? V_0, short? V_1) IL_0000: ldsfld ""short? Program.sh"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool short?.HasValue.get"" IL_000d: brtrue.s IL_001a IL_000f: ldloca.s V_1 IL_0011: initobj ""short?"" IL_0017: ldloc.1 IL_0018: br.s IL_0029 IL_001a: ldloca.s V_0 IL_001c: call ""short short?.GetValueOrDefault()"" IL_0021: ldc.i4.1 IL_0022: add IL_0023: conv.i2 IL_0024: newobj ""short?..ctor(short)"" IL_0029: dup IL_002a: stsfld ""short? Program.sh"" IL_002f: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); } [Fact] public void TestNullableComparisonOpsBothAlwaysNull() { // An ==, !=, <, >, <= or >= operation where both operands // are null is always true for equality, and always false otherwise. // Note that the native compiler has a bug; it does not produce the warning // "comparing null with S? always produces false" -- it incorrectly warns // that it produces a null of type bool? ! Roslyn does not reproduce this bug. string source = @" struct S // User-defined relational ops { public static bool operator ==(S x, S y) { return true; } public static bool operator !=(S x, S y) { return true; } public static bool operator <(S x, S y) { return true; } public static bool operator >(S x, S y) { return true; } public static bool operator <=(S x, S y) { return true; } public static bool operator >=(S x, S y) { return true; } public override bool Equals(object x) { return true; } public override int GetHashCode() { return 0; } } struct T // no user-defined relational ops { } class Program { static bool M1() { return new int?() == new short?(); } static bool M2() { return default(double?) != new short?(); } static bool M3() { return ((int?)null) < new decimal?(); } static bool M4() { return new S?() == new S?(); } static bool M5() { return default(S?) != new S?(); } static bool M6() { return ((S?)null) < new S?(); } static bool M7() // Special case for equality with null literal and no overloaded operators. { return default(T?) == null; } static bool M8() { return null != new T?(); } static void Main() {} } "; string expectedOutput = ""; string expectedILTrue = @"{ // Code size 2 (0x2) .maxstack 1 IL_0000: ldc.i4.1 IL_0001: ret }"; string expectedILFalse = @"{ // Code size 2 (0x2) .maxstack 1 IL_0000: ldc.i4.0 IL_0001: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (25,16): warning CS0464: Comparing with null of type 'decimal?' always produces 'false' // return ((int?)null) < new decimal?(); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((int?)null) < new decimal?()").WithArguments("decimal?"), // (37,16): warning CS0464: Comparing with null of type 'S?' always produces 'false' // return ((S?)null) < new S?(); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((S?)null) < new S?()").WithArguments("S?")); comp.VerifyIL("Program.M1", expectedILTrue); comp.VerifyIL("Program.M2", expectedILFalse); comp.VerifyIL("Program.M3", expectedILFalse); comp.VerifyIL("Program.M4", expectedILTrue); comp.VerifyIL("Program.M5", expectedILFalse); comp.VerifyIL("Program.M6", expectedILFalse); comp.VerifyIL("Program.M7", expectedILTrue); comp.VerifyIL("Program.M8", expectedILFalse); } [Fact] public void TestNullableComparisonNonNullWithLiteralNull() { // We can optimize this away to simply evaluating N() for its side effects // and returning false. string source = @" struct S {} class Program { static S N() { System.Console.WriteLine(123); return new S(); } static bool M() { return new S?(N()) == null; } static void Main() {M();} } "; string expectedOutput = "123"; string expectedIL = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""S Program.N()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyIL("Program.M", expectedIL); } [Fact] public void TestNullableComparisonOpsBothNeverNull() { // An ==, !=, <, >, <= or >= operation where both operands // are not null simply drops the lifting logic entirely. string source = @" struct S // User-defined relational ops { public static bool operator ==(S x, S y) { return true; } public static bool operator !=(S x, S y) { return true; } public static bool operator <(S x, S y) { return true; } public static bool operator >(S x, S y) { return true; } public static bool operator <=(S x, S y) { return true; } public static bool operator >=(S x, S y) { return true; } public override bool Equals(object x) { return true; } public override int GetHashCode() { return 0; } } class Program { static int N1() { return 123; } static short N2() { return 123; } static double N3() { return 123; } static decimal N4() { return 123; } static S N5() { return new S(); } static bool M1() { // Notice that there are two optimizations here and in the next few cases. // First we optimize the conversion from short? to int? so that the right // hand side is new int?((int)N2()). Second, we optimize the comparison to // N1() == (int)N2(), eliminating all the lifting. return new int?(N1()) == new short?(N2()); } static bool M2() { return new double?(N3()) != new short?(N2()); } static bool M3() { return new int?(N1()) < new decimal?(N4()); } static bool M4() { return new S?(N5()) == new S?(N5()); } static bool M5() { return new S?(N5()) != new S?(N5()); } static bool M6() { return new S?(N5()) < new S?(N5()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 13 (0xd) .maxstack 2 IL_0000: call ""int Program.N1()"" IL_0005: call ""short Program.N2()"" IL_000a: ceq IL_000c: ret }"; string expectedIL2 = @"{ // Code size 17 (0x11) .maxstack 2 IL_0000: call ""double Program.N3()"" IL_0005: call ""short Program.N2()"" IL_000a: conv.r8 IL_000b: ceq IL_000d: ldc.i4.0 IL_000e: ceq IL_0010: ret }"; string expectedIL3 = @"{ // Code size 21 (0x15) .maxstack 2 IL_0000: call ""int Program.N1()"" IL_0005: call ""decimal decimal.op_Implicit(int)"" IL_000a: call ""decimal Program.N4()"" IL_000f: call ""bool decimal.op_LessThan(decimal, decimal)"" IL_0014: ret } "; string expectedIL4 = @"{ // Code size 16 (0x10) .maxstack 2 IL_0000: call ""S Program.N5()"" IL_0005: call ""S Program.N5()"" IL_000a: call ""bool S.op_Equality(S, S)"" IL_000f: ret }"; string expectedIL5 = @"{ // Code size 16 (0x10) .maxstack 2 IL_0000: call ""S Program.N5()"" IL_0005: call ""S Program.N5()"" IL_000a: call ""bool S.op_Inequality(S, S)"" IL_000f: ret }"; string expectedIL6 = @"{ // Code size 16 (0x10) .maxstack 2 IL_0000: call ""S Program.N5()"" IL_0005: call ""S Program.N5()"" IL_000a: call ""bool S.op_LessThan(S, S)"" IL_000f: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); comp.VerifyIL("Program.M5", expectedIL5); comp.VerifyIL("Program.M6", expectedIL6); } [Fact, WorkItem(663, "https://github.com/dotnet/roslyn/issues/663")] public void TestNullableComparisonOpsOneNullOneNonNull() { // An ==, !=, <, >, <= or >= operation where one operand is null and the // other is non-null is always false except for inequality, which is true. // We can skip the lifting and just generate the side effect. // Note that Roslyn produces considerably more warnings here than the // native compiler; the native compiler only produces warnings for // "((int?)null) < new decimal?(N3())" and "((S?)null) < new S?(N4())". // For compatibility Roslyn reports the same diagnostics by default, // but in "strict" mode (which will be part of the "warning waves" once // we do that) Roslyn will report warnings for // new S() == null and new S() != null. string source = @" struct S // User-defined relational ops { public static bool operator ==(S x, S y) { return true; } public static bool operator !=(S x, S y) { return true; } public static bool operator <(S x, S y) { return true; } public static bool operator >(S x, S y) { return true; } public static bool operator <=(S x, S y) { return true; } public static bool operator >=(S x, S y) { return true; } public override bool Equals(object x) { return true; } public override int GetHashCode() { return 0; } } class Program { static int N1() { return 1; } static short N2() { return 1; } static decimal N3() { return 1; } static S N4() { return new S(); } static bool M1() { return new int?(N1()) == new short?(); } static bool M2() { return default(double?) != new short?(N2()); } static bool M3() { return ((int?)null) < new decimal?(N3()); } static bool M4() { return new S?() == new S?(N4()); } static bool M5() { return default(S?) != new S?(N4()); } static bool M6() { return ((S?)null) < new S?(N4()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""int Program.N1()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; string expectedIL2 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""short Program.N2()"" IL_0005: pop IL_0006: ldc.i4.1 IL_0007: ret }"; string expectedIL3 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""decimal Program.N3()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; string expectedIL4 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""S Program.N4()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; string expectedIL5 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""S Program.N4()"" IL_0005: pop IL_0006: ldc.i4.1 IL_0007: ret }"; string expectedIL6 = expectedIL4; CompileAndVerify(source, expectedOutput: expectedOutput).VerifyDiagnostics( // (21,16): warning CS0472: The result of the expression is always 'false' since a value of type 'int' is never equal to 'null' of type 'short?' // return new int?(N1()) == new short?(); Diagnostic(ErrorCode.WRN_NubExprIsConstBool, "new int?(N1()) == new short?()").WithArguments("false", "int", "short?").WithLocation(21, 16), // (25,16): warning CS0472: The result of the expression is always 'true' since a value of type 'double' is never equal to 'null' of type 'double?' // return default(double?) != new short?(N2()); Diagnostic(ErrorCode.WRN_NubExprIsConstBool, "default(double?) != new short?(N2())").WithArguments("true", "double", "double?").WithLocation(25, 16), // (29,16): warning CS0464: Comparing with null of type 'int?' always produces 'false' // return ((int?)null) < new decimal?(N3()); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((int?)null) < new decimal?(N3())").WithArguments("int?").WithLocation(29, 16), // (41,16): warning CS0464: Comparing with null of type 'S?' always produces 'false' // return ((S?)null) < new S?(N4()); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((S?)null) < new S?(N4())").WithArguments("S?").WithLocation(41, 16) ); var comp = CompileAndVerify(source, expectedOutput: expectedOutput, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular.WithStrictFeature()); comp.VerifyDiagnostics( // (21,16): warning CS0472: The result of the expression is always 'false' since a value of type 'int' is never equal to 'null' of type 'short?' // return new int?(N1()) == new short?(); Diagnostic(ErrorCode.WRN_NubExprIsConstBool, "new int?(N1()) == new short?()").WithArguments("false", "int", "short?").WithLocation(21, 16), // (25,16): warning CS0472: The result of the expression is always 'true' since a value of type 'double' is never equal to 'null' of type 'double?' // return default(double?) != new short?(N2()); Diagnostic(ErrorCode.WRN_NubExprIsConstBool, "default(double?) != new short?(N2())").WithArguments("true", "double", "double?").WithLocation(25, 16), // (29,16): warning CS0464: Comparing with null of type 'int?' always produces 'false' // return ((int?)null) < new decimal?(N3()); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((int?)null) < new decimal?(N3())").WithArguments("int?").WithLocation(29, 16), // (33,16): warning CS8073: The result of the expression is always 'false' since a value of type 'S' is never equal to 'null' of type 'S?' // return new S?() == new S?(N4()); Diagnostic(ErrorCode.WRN_NubExprIsConstBool2, "new S?() == new S?(N4())").WithArguments("false", "S", "S?").WithLocation(33, 16), // (37,16): warning CS8073: The result of the expression is always 'true' since a value of type 'S' is never equal to 'null' of type 'S?' // return default(S?) != new S?(N4()); Diagnostic(ErrorCode.WRN_NubExprIsConstBool2, "default(S?) != new S?(N4())").WithArguments("true", "S", "S?").WithLocation(37, 16), // (41,16): warning CS0464: Comparing with null of type 'S?' always produces 'false' // return ((S?)null) < new S?(N4()); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((S?)null) < new S?(N4())").WithArguments("S?").WithLocation(41, 16) ); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); comp.VerifyIL("Program.M5", expectedIL5); comp.VerifyIL("Program.M6", expectedIL6); } [Fact] public void TestNullableComparisonOpsOneNullOneUnknown() { // An <, >, <= or >= operation where one operand is null and the // other is unknown is always false; we can skip the lifting and // generate the side effect. // // An == or != operation where one operand is null and the other is // unknown turns into a call to HasValue. // // As mentioned above, the native compiler gets one of the warnings wrong; // Roslyn gets it right. string source = @" struct S // User-defined relational ops { public static bool operator ==(S x, S y) { return true; } public static bool operator !=(S x, S y) { return true; } public static bool operator <(S x, S y) { return true; } public static bool operator >(S x, S y) { return true; } public static bool operator <=(S x, S y) { return true; } public static bool operator >=(S x, S y) { return true; } public override bool Equals(object x) { return true; } public override int GetHashCode() { return 0; } } class Program { static int? N1() { return 1; } static short? N2() { return 1; } static decimal? N3() { return 1; } static S? N4() { return new S(); } static bool M1() { return N1() == new short?(); } static bool M2() { return default(double?) != N2(); } static bool M3() { return ((int?)null) < N3(); } static bool M4() { return new S?() == N4(); } static bool M5() { return default(S?) != N4(); } static bool M6() { return ((S?)null) < N4(); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 17 (0x11) .maxstack 2 .locals init (int? V_0) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool int?.HasValue.get"" IL_000d: ldc.i4.0 IL_000e: ceq IL_0010: ret }"; // TODO: Roslyn and the native compiler both produce this sub-optimal code for // TODO: "default(double?) != N2()". We are essentially generating: // TODO: // TODO: short? temp1 = N2(); // TODO: double? temp2 = temp1.HasValue ? new double?((double)temp1.GetValueOrDefault()) : new double?(); // TODO: return temp2.HasValue; // TODO: // TODO: We could be instead simply generating // TODO: // TODO: return N2().HasValue(); string expectedIL2 = @"{ // Code size 48 (0x30) .maxstack 1 .locals init (short? V_0, double? V_1) IL_0000: call ""short? Program.N2()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool short?.HasValue.get"" IL_000d: brtrue.s IL_001a IL_000f: ldloca.s V_1 IL_0011: initobj ""double?"" IL_0017: ldloc.1 IL_0018: br.s IL_0027 IL_001a: ldloca.s V_0 IL_001c: call ""short short?.GetValueOrDefault()"" IL_0021: conv.r8 IL_0022: newobj ""double?..ctor(double)"" IL_0027: stloc.1 IL_0028: ldloca.s V_1 IL_002a: call ""bool double?.HasValue.get"" IL_002f: ret }"; string expectedIL3 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""decimal? Program.N3()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; string expectedIL4 = @"{ // Code size 17 (0x11) .maxstack 2 .locals init (S? V_0) IL_0000: call ""S? Program.N4()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool S?.HasValue.get"" IL_000d: ldc.i4.0 IL_000e: ceq IL_0010: ret }"; string expectedIL5 = @"{ // Code size 14 (0xe) .maxstack 1 .locals init (S? V_0) IL_0000: call ""S? Program.N4()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool S?.HasValue.get"" IL_000d: ret }"; string expectedIL6 = @"{ // Code size 8 (0x8) .maxstack 1 IL_0000: call ""S? Program.N4()"" IL_0005: pop IL_0006: ldc.i4.0 IL_0007: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (29,16): warning CS0464: Comparing with null of type 'int?' always produces 'false' // return ((int?)null) < N3(); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((int?)null) < N3()").WithArguments("int?"), // (41,16): warning CS0464: Comparing with null of type 'S?' always produces 'false' // return ((S?)null) < N4(); Diagnostic(ErrorCode.WRN_CmpAlwaysFalse, "((S?)null) < N4()").WithArguments("S?") ); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); comp.VerifyIL("Program.M5", expectedIL5); comp.VerifyIL("Program.M6", expectedIL6); } [Fact] public void TestNullableComparisonOpsOneNonNullOneUnknown() { // When we have a lifted comparison where we know that one side // is definitely not null, but know nothing about the other, then // we make a slight modification to the code generation. For example, // suppose X() and Y() return int?. For "return X() < Y();" we would generate: // int? x = X(); // int? y = Y(); // return x.GetValueOrDefault() < y.GetValueOrDefault() ? x.HasValue & y.HasValue : false; // // But suppose Z() returns int. For X() < Z(), rather than converting Z() to int? and doing the // same codegen as before, we simplify the codegen to: // // int? x = X(); // int z = Z(); // return x.GetValueOrDefault() < z ? x.HasValue : false; // // We apply this same pattern to all lifted comparison operators. string source = @" struct S // User-defined relational ops { public static bool operator ==(S x, S y) { return true; } public static bool operator !=(S x, S y) { return true; } public static bool operator <(S x, S y) { return true; } public static bool operator >(S x, S y) { return true; } public static bool operator <=(S x, S y) { return true; } public static bool operator >=(S x, S y) { return true; } public override bool Equals(object x) { return true; } public override int GetHashCode() { return 0; } } class Program { static int? N1() { return 1; } static short? N2() { return 1; } static decimal? N3() { return 1; } static S? N4() { return new S(); } static int V1() { return 1; } static short V2() { return 1; } static decimal V3() { return 1; } static S V4() { return new S(); } static bool M1() { return N1() == new short?(V2()); } static bool M2() { return N1() < new decimal?(V3()); } static bool M3() { return new S?(V4()) == N4(); } static bool M4() { return new S?(V4()) != N4(); } static bool M5() { return new S?(V4()) < N4(); } static void Main() { } } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 32 (0x20) .maxstack 2 .locals init (int? V_0, int V_1) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.0 IL_0006: call ""short Program.V2()"" IL_000b: stloc.1 IL_000c: ldloca.s V_0 IL_000e: call ""int int?.GetValueOrDefault()"" IL_0013: ldloc.1 IL_0014: beq.s IL_0018 IL_0016: ldc.i4.0 IL_0017: ret IL_0018: ldloca.s V_0 IL_001a: call ""bool int?.HasValue.get"" IL_001f: ret }"; // TODO: We do a worse job than the native compiler here. Find out why. string expectedIL2 = @"{ // Code size 75 (0x4b) .maxstack 2 .locals init (decimal? V_0, decimal V_1, int? V_2, decimal? V_3) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.2 IL_0006: ldloca.s V_2 IL_0008: call ""bool int?.HasValue.get"" IL_000d: brtrue.s IL_001a IL_000f: ldloca.s V_3 IL_0011: initobj ""decimal?"" IL_0017: ldloc.3 IL_0018: br.s IL_002b IL_001a: ldloca.s V_2 IL_001c: call ""int int?.GetValueOrDefault()"" IL_0021: call ""decimal decimal.op_Implicit(int)"" IL_0026: newobj ""decimal?..ctor(decimal)"" IL_002b: stloc.0 IL_002c: call ""decimal Program.V3()"" IL_0031: stloc.1 IL_0032: ldloca.s V_0 IL_0034: call ""decimal decimal?.GetValueOrDefault()"" IL_0039: ldloc.1 IL_003a: call ""bool decimal.op_LessThan(decimal, decimal)"" IL_003f: brtrue.s IL_0043 IL_0041: ldc.i4.0 IL_0042: ret IL_0043: ldloca.s V_0 IL_0045: call ""bool decimal?.HasValue.get"" IL_004a: ret }"; string expectedIL3 = @"{ // Code size 37 (0x25) .maxstack 2 .locals init (S V_0, S? V_1) IL_0000: call ""S Program.V4()"" IL_0005: stloc.0 IL_0006: call ""S? Program.N4()"" IL_000b: stloc.1 IL_000c: ldloca.s V_1 IL_000e: call ""bool S?.HasValue.get"" IL_0013: brtrue.s IL_0017 IL_0015: ldc.i4.0 IL_0016: ret IL_0017: ldloc.0 IL_0018: ldloca.s V_1 IL_001a: call ""S S?.GetValueOrDefault()"" IL_001f: call ""bool S.op_Equality(S, S)"" IL_0024: ret }"; string expectedIL4 = @"{ // Code size 37 (0x25) .maxstack 2 .locals init (S V_0, S? V_1) IL_0000: call ""S Program.V4()"" IL_0005: stloc.0 IL_0006: call ""S? Program.N4()"" IL_000b: stloc.1 IL_000c: ldloca.s V_1 IL_000e: call ""bool S?.HasValue.get"" IL_0013: brtrue.s IL_0017 IL_0015: ldc.i4.1 IL_0016: ret IL_0017: ldloc.0 IL_0018: ldloca.s V_1 IL_001a: call ""S S?.GetValueOrDefault()"" IL_001f: call ""bool S.op_Inequality(S, S)"" IL_0024: ret }"; string expectedIL5 = @"{ // Code size 37 (0x25) .maxstack 2 .locals init (S V_0, S? V_1) IL_0000: call ""S Program.V4()"" IL_0005: stloc.0 IL_0006: call ""S? Program.N4()"" IL_000b: stloc.1 IL_000c: ldloca.s V_1 IL_000e: call ""bool S?.HasValue.get"" IL_0013: brtrue.s IL_0017 IL_0015: ldc.i4.0 IL_0016: ret IL_0017: ldloc.0 IL_0018: ldloca.s V_1 IL_001a: call ""S S?.GetValueOrDefault()"" IL_001f: call ""bool S.op_LessThan(S, S)"" IL_0024: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); comp.VerifyIL("Program.M5", expectedIL5); } [Fact] public void TestLiftedConversionOnTopOfLifted() { // Here's an optimization that the dev10 compiler does not do except in some special cases. // If we have a lifted conversion "on top" of another lifted operation, then the lifted // conversion can be "distributed" to both branches of the underlying lifted operation. // // For example, suppose we have // // return (double?)(N1() + N2()); // // Where N1 and N2 return int?. The dev10 compiler does this in two steps: first it // computes the int? result of the addition, and then it converts that int? to double? // with a lifted conversion. Basically, it generates: // // int? t1 = N1(); // int? t2 = N2(); // int? t3 = t1.HasValue && t2.HasValue ? new int?(t1.Value + t2.Value) : new int?(); // double? t4 = t3.HasValue ? new double?((double)t3.Value)) : new double?(); // // But t3 is completely unnecessary here. We observe that the lifted conversion: // // (double?) (t1.HasValue && t2.HasValue ? new int?(t1.Value + t2.Value) : new int?()) // // Is the same as distributing the conversion to the consequence and alternative: // // (t1.HasValue && t2.HasValue ? (double?)( new int?(t1.Value + t2.Value) ): (double?) ( new int?() ) ) // // And now we can optimize the consequence and alternative down to // // (t1.HasValue && t2.HasValue ? new double?((double)(t1.Value + t2.Value) ): new double?() ) // // And the int? t3 disappears entirely. // // This optimization has the nice property that it composes well with itself. string source = @" struct S { public static S operator -(S s) { return s; } public static implicit operator int(S s) { return 1; } } class Program { static int? N1() { return 1; } static int? N2() { return 1; } static S? N3() { return null; } // Start with a nice simple case; we have a lifted numeric conversion on top of a // lifted addition. static long? M1() { return N1() + N2(); } // This should also work with lifted user-defined conversions, and work on top // of a lifted unary operator. static int? M2() { return -N3(); } static void Main() { } } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 61 (0x3d) .maxstack 2 .locals init (int? V_0, int? V_1, long? V_2) IL_0000: call ""int? Program.N1()"" IL_0005: stloc.0 IL_0006: call ""int? Program.N2()"" IL_000b: stloc.1 IL_000c: ldloca.s V_0 IL_000e: call ""bool int?.HasValue.get"" IL_0013: ldloca.s V_1 IL_0015: call ""bool int?.HasValue.get"" IL_001a: and IL_001b: brtrue.s IL_0027 IL_001d: ldloca.s V_2 IL_001f: initobj ""long?"" IL_0025: ldloc.2 IL_0026: ret IL_0027: ldloca.s V_0 IL_0029: call ""int int?.GetValueOrDefault()"" IL_002e: ldloca.s V_1 IL_0030: call ""int int?.GetValueOrDefault()"" IL_0035: add IL_0036: conv.i8 IL_0037: newobj ""long?..ctor(long)"" IL_003c: ret }"; string expectedIL2 = @"{ // Code size 48 (0x30) .maxstack 1 .locals init (S? V_0, int? V_1) IL_0000: call ""S? Program.N3()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool S?.HasValue.get"" IL_000d: brtrue.s IL_0019 IL_000f: ldloca.s V_1 IL_0011: initobj ""int?"" IL_0017: ldloc.1 IL_0018: ret IL_0019: ldloca.s V_0 IL_001b: call ""S S?.GetValueOrDefault()"" IL_0020: call ""S S.op_UnaryNegation(S)"" IL_0025: call ""int S.op_Implicit(S)"" IL_002a: newobj ""int?..ctor(int)"" IL_002f: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); } [Fact] public void TestNullableBoolBinOpsBothAlwaysNull() { // x & y and x | y are null if both operands are null. string source = @" class Program { static bool? M1() { return new bool?() | new bool?(); } static bool? M2() { return (bool?)null & default(bool?); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (bool? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""bool?"" IL_0008: ldloc.0 IL_0009: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL); comp.VerifyIL("Program.M2", expectedIL); } [Fact] public void TestNullableBoolBinOpsBothNotNull() { // x & y and x | y can be reduced to their non-lifted forms // if both operands are known to be non-null. // // Roslyn does a slightly better job than the native compiler here. // The native compiler effectively generates code as though you'd written: // // bool temp1 = N(); // bool? temp2 = new bool?(N() & N()); // return temp1 ? new bool?(true) : temp2; // // Whereas Roslyn simply generates code as though you'd written: // // return new bool?(N() | N() & N()) string source = @" class Program { static bool N() { return true; } static bool? M1() { return new bool?(N()) | new bool?(N()) & new bool?(N()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 23 (0x17) .maxstack 3 IL_0000: call ""bool Program.N()"" IL_0005: call ""bool Program.N()"" IL_000a: call ""bool Program.N()"" IL_000f: and IL_0010: or IL_0011: newobj ""bool?..ctor(bool)"" IL_0016: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL); } [Fact] public void TestNullableBoolBinOpsOneNull() { // codegen for x & y and x | y can be simplified if one operand is known to be null, // and simplified even further if the other operand is known to be non-null. string source = @" class Program { static bool? N() { return false; } static bool B() { return false; } static bool? M1() { // Generated as temp = N(), temp.GetValueOrDefault() ? null : temp return N() & new bool?(); } static bool? M2() { // Generated as temp = N(), temp.GetValueOrDefault() ? null : temp return default(bool?) & N(); } static bool? M3() { // Generated as temp = N(), temp.GetValueOrDefault() ? temp : null return N() | new bool?(); } static bool? M4() { // Generated as temp = N(), temp.GetValueOrDefault() ? temp : null return default(bool?) | N(); } static bool? M5() { // Generated as B() ? null : new bool?(false) return new bool?(B()) & new bool?(); } static bool? M6() { // Generated as B() ? null : new bool?(false) return default(bool?) & new bool?(B()); } static bool? M7() { // Generated as B() ? new bool?(true) : null return new bool?(B()) | new bool?(); } static bool? M8() { // Generated as B() ? new bool?(true) : null return default(bool?) | new bool?(B()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 27 (0x1b) .maxstack 1 .locals init (bool? V_0, bool? V_1) IL_0000: call ""bool? Program.N()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool bool?.GetValueOrDefault()"" IL_000d: brtrue.s IL_0011 IL_000f: ldloc.0 IL_0010: ret IL_0011: ldloca.s V_1 IL_0013: initobj ""bool?"" IL_0019: ldloc.1 IL_001a: ret }"; string expectedIL2 = expectedIL1; string expectedIL3 = @"{ // Code size 27 (0x1b) .maxstack 1 .locals init (bool? V_0, bool? V_1) IL_0000: call ""bool? Program.N()"" IL_0005: stloc.0 IL_0006: ldloca.s V_0 IL_0008: call ""bool bool?.GetValueOrDefault()"" IL_000d: brtrue.s IL_0019 IL_000f: ldloca.s V_1 IL_0011: initobj ""bool?"" IL_0017: ldloc.1 IL_0018: ret IL_0019: ldloc.0 IL_001a: ret }"; string expectedIL4 = expectedIL3; string expectedIL5 = @"{ // Code size 24 (0x18) .maxstack 1 .locals init (bool? V_0) IL_0000: call ""bool Program.B()"" IL_0005: brtrue.s IL_000e IL_0007: ldc.i4.0 IL_0008: newobj ""bool?..ctor(bool)"" IL_000d: ret IL_000e: ldloca.s V_0 IL_0010: initobj ""bool?"" IL_0016: ldloc.0 IL_0017: ret }"; string expectedIL6 = expectedIL5; string expectedIL7 = @"{ // Code size 24 (0x18) .maxstack 1 .locals init (bool? V_0) IL_0000: call ""bool Program.B()"" IL_0005: brtrue.s IL_0011 IL_0007: ldloca.s V_0 IL_0009: initobj ""bool?"" IL_000f: ldloc.0 IL_0010: ret IL_0011: ldc.i4.1 IL_0012: newobj ""bool?..ctor(bool)"" IL_0017: ret } "; string expectedIL8 = expectedIL7; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); comp.VerifyIL("Program.M5", expectedIL5); comp.VerifyIL("Program.M6", expectedIL6); comp.VerifyIL("Program.M7", expectedIL7); comp.VerifyIL("Program.M8", expectedIL8); } [Fact] public void TestNullableBoolBinOpsOneNonNull() { // Codegen for x & y and x | y can be simplified if one operand is known to be non null. // Note that we have already considered the case where one operand is null and the // other is non null, in the test case above. string source = @" class Program { static bool? N() { return false; } static bool B() { return false; } static bool? M1() { return new bool?(B()) & N(); } static bool? M2() { return N() & new bool?(B()); } static bool? M3() { return new bool?(B()) | N(); } static bool? M4() { return N() | new bool?(B()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 22 (0x16) .maxstack 1 .locals init (bool? V_0) IL_0000: call ""bool? Program.N()"" IL_0005: stloc.0 IL_0006: call ""bool Program.B()"" IL_000b: brtrue.s IL_0014 IL_000d: ldc.i4.0 IL_000e: newobj ""bool?..ctor(bool)"" IL_0013: ret IL_0014: ldloc.0 IL_0015: ret }"; string expectedIL2 = @"{ // Code size 22 (0x16) .maxstack 2 .locals init (bool? V_0) IL_0000: call ""bool Program.B()"" IL_0005: call ""bool? Program.N()"" IL_000a: stloc.0 IL_000b: brtrue.s IL_0014 IL_000d: ldc.i4.0 IL_000e: newobj ""bool?..ctor(bool)"" IL_0013: ret IL_0014: ldloc.0 IL_0015: ret }"; string expectedIL3 = @"{ // Code size 22 (0x16) .maxstack 1 .locals init (bool? V_0) IL_0000: call ""bool? Program.N()"" IL_0005: stloc.0 IL_0006: call ""bool Program.B()"" IL_000b: brtrue.s IL_000f IL_000d: ldloc.0 IL_000e: ret IL_000f: ldc.i4.1 IL_0010: newobj ""bool?..ctor(bool)"" IL_0015: ret }"; string expectedIL4 = @"{ // Code size 22 (0x16) .maxstack 2 .locals init (bool? V_0) IL_0000: call ""bool Program.B()"" IL_0005: call ""bool? Program.N()"" IL_000a: stloc.0 IL_000b: brtrue.s IL_000f IL_000d: ldloc.0 IL_000e: ret IL_000f: ldc.i4.1 IL_0010: newobj ""bool?..ctor(bool)"" IL_0015: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); comp.VerifyIL("Program.M3", expectedIL3); comp.VerifyIL("Program.M4", expectedIL4); } [Fact] public void TestNullableBinOpsBothAlwaysNull() { // x op y is null if both ops are null for the binary operators // * / % + - << >> and for non-bool & ^ | string source = @" class Program { static long? M1() { return new int?() + new long?(); } static decimal? M2() { return (short?)null * default(decimal?); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (long? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""long?"" IL_0008: ldloc.0 IL_0009: ret }"; string expectedIL2 = @"{ // Code size 10 (0xa) .maxstack 1 .locals init (decimal? V_0) IL_0000: ldloca.s V_0 IL_0002: initobj ""decimal?"" IL_0008: ldloc.0 IL_0009: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (6,16): warning CS0458: The result of the expression is always 'null' of type 'long?' // return new int?() + new long?(); Diagnostic(ErrorCode.WRN_AlwaysNull, "new int?() + new long?()").WithArguments("long?"), // (10,16): warning CS0458: The result of the expression is always 'null' of type 'decimal?' // return (short?)null * default(decimal?); Diagnostic(ErrorCode.WRN_AlwaysNull, "(short?)null * default(decimal?)").WithArguments("decimal?")); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); } [Fact] public void TestNullableBinOpsBothNonNull() { // Lifted x op y is generated as non-lifted if both operands are known to be non-null // for operators * / % + - << >> and for non-bool & ^ | // // Roslyn does a far better job of this optimization than the native compiler. string source = @" class Program { static int N() { return 1; } static int? M1() { return new int?(N()) * new int?(N()) / new int?(N()) % new int?(N()) + new int?(N()) - new int?(N()) << new int?(N()) >> new int?(N()) & new int?(N()) ^ new int?(N()) | new int?(N()); } static void Main() {} } "; string expectedOutput = ""; string expectedIL = @"{ // Code size 77 (0x4d) .maxstack 3 IL_0000: call ""int Program.N()"" IL_0005: call ""int Program.N()"" IL_000a: mul IL_000b: call ""int Program.N()"" IL_0010: div IL_0011: call ""int Program.N()"" IL_0016: rem IL_0017: call ""int Program.N()"" IL_001c: add IL_001d: call ""int Program.N()"" IL_0022: sub IL_0023: call ""int Program.N()"" IL_0028: ldc.i4.s 31 IL_002a: and IL_002b: shl IL_002c: call ""int Program.N()"" IL_0031: ldc.i4.s 31 IL_0033: and IL_0034: shr IL_0035: call ""int Program.N()"" IL_003a: and IL_003b: call ""int Program.N()"" IL_0040: xor IL_0041: call ""int Program.N()"" IL_0046: or IL_0047: newobj ""int?..ctor(int)"" IL_004c: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL); } [Fact] public void TestNullableBinOpsOneNull() { // If we have null + N() or null + new int?(B()) // then we simply generate M() as a side effect and result in null. string source = @" class Program { static int? N() { return 1; } static int B() { return 1; } static int? M1() { return new int?() + N(); } static int? M2() { return new int?(B()) * default(int?); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 16 (0x10) .maxstack 1 .locals init (int? V_0) IL_0000: call ""int? Program.N()"" IL_0005: pop IL_0006: ldloca.s V_0 IL_0008: initobj ""int?"" IL_000e: ldloc.0 IL_000f: ret }"; string expectedIL2 = @"{ // Code size 16 (0x10) .maxstack 1 .locals init (int? V_0) IL_0000: call ""int Program.B()"" IL_0005: pop IL_0006: ldloca.s V_0 IL_0008: initobj ""int?"" IL_000e: ldloc.0 IL_000f: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics( // (9,16): warning CS0458: The result of the expression is always 'null' of type 'int?' // return new int?() + N(); Diagnostic(ErrorCode.WRN_AlwaysNull, "new int?() + N()").WithArguments("int?"), // (13,16): warning CS0458: The result of the expression is always 'null' of type 'int?' // return new int?(B()) * default(int?); Diagnostic(ErrorCode.WRN_AlwaysNull, "new int?(B()) * default(int?)").WithArguments("int?")); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); } [Fact] public void TestNullableBinOpsOneNonNull() { // If one side of the nullable binop is non-null then we skip calling HasValue and GetValueOrDefault // on that side. string source = @" class Program { static int B() { return 1; } static int? N() { return 1; } static int? M1() { return new int?(B()) + N(); } static int? M2() { return new int?(1) + N(); } static void Main() {} } "; string expectedOutput = ""; string expectedIL1 = @"{ // Code size 46 (0x2e) .maxstack 2 .locals init (int V_0, int? V_1, int? V_2) IL_0000: call ""int Program.B()"" IL_0005: stloc.0 IL_0006: call ""int? Program.N()"" IL_000b: stloc.1 IL_000c: ldloca.s V_1 IL_000e: call ""bool int?.HasValue.get"" IL_0013: brtrue.s IL_001f IL_0015: ldloca.s V_2 IL_0017: initobj ""int?"" IL_001d: ldloc.2 IL_001e: ret IL_001f: ldloc.0 IL_0020: ldloca.s V_1 IL_0022: call ""int int?.GetValueOrDefault()"" IL_0027: add IL_0028: newobj ""int?..ctor(int)"" IL_002d: ret }"; // TODO: Roslyn does a slightly worse job here than the native compiler does. // TODO: The native compiler knows that the constant need not be stored in a temporary. // TODO: We will clean this up in a later checkin. // TODO: When we do so, add tests for ++ -- +=, etc. string expectedIL2 = @"{ // Code size 42 (0x2a) .maxstack 2 .locals init (int V_0, int? V_1, int? V_2) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: call ""int? Program.N()"" IL_0007: stloc.1 IL_0008: ldloca.s V_1 IL_000a: call ""bool int?.HasValue.get"" IL_000f: brtrue.s IL_001b IL_0011: ldloca.s V_2 IL_0013: initobj ""int?"" IL_0019: ldloc.2 IL_001a: ret IL_001b: ldloc.0 IL_001c: ldloca.s V_1 IL_001e: call ""int int?.GetValueOrDefault()"" IL_0023: add IL_0024: newobj ""int?..ctor(int)"" IL_0029: ret }"; var comp = CompileAndVerify(source, expectedOutput: expectedOutput); comp.VerifyDiagnostics(); comp.VerifyIL("Program.M1", expectedIL1); comp.VerifyIL("Program.M2", expectedIL2); } } }