diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs index 10438a88f7954..bc6a36f9cfd7c 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs @@ -675,29 +675,32 @@ private BoundCollectionExpression BindCollectionInitializerCollectionExpression( } var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true }; - var collectionInitializerAddMethodBinder = this.WithAdditionalFlags(BinderFlags.CollectionInitializerAddMethod); var builder = ArrayBuilder.GetInstance(node.Elements.Length); - foreach (var element in node.Elements) + if (node.Elements.Length > 0) { - var result = element switch + var collectionInitializerAddMethodBinder = this.WithAdditionalFlags(BinderFlags.CollectionInitializerAddMethod); + foreach (var element in node.Elements) { - BoundBadExpression => element, - BoundCollectionExpressionSpreadElement spreadElement => BindCollectionInitializerSpreadElementAddMethod( - (SpreadElementSyntax)spreadElement.Syntax, - spreadElement, + var result = element switch + { + BoundBadExpression => element, + BoundCollectionExpressionSpreadElement spreadElement => BindCollectionInitializerSpreadElementAddMethod( + (SpreadElementSyntax)spreadElement.Syntax, + spreadElement, + collectionInitializerAddMethodBinder, + implicitReceiver, + diagnostics), + _ => BindCollectionInitializerElementAddMethod( + (ExpressionSyntax)element.Syntax, + ImmutableArray.Create(element), + hasEnumerableInitializerType: true, collectionInitializerAddMethodBinder, - implicitReceiver, - diagnostics), - _ => BindCollectionInitializerElementAddMethod( - (ExpressionSyntax)element.Syntax, - ImmutableArray.Create(element), - hasEnumerableInitializerType: true, - collectionInitializerAddMethodBinder, - diagnostics, - implicitReceiver), - }; - result.WasCompilerGenerated = true; - builder.Add(result); + diagnostics, + implicitReceiver), + }; + result.WasCompilerGenerated = true; + builder.Add(result); + } } return new BoundCollectionExpression( syntax, @@ -718,7 +721,6 @@ private BoundCollectionExpression BindListInterfaceCollectionExpression( TypeSymbol elementType, BindingDiagnosticBag diagnostics) { - // https://github.com/dotnet/roslyn/issues/68785: Emit [] as Array.Empty() rather than a List. var result = BindCollectionInitializerCollectionExpression( node, CollectionExpressionTypeKind.ListInterface, diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index 18bfe8e4c69a5..862ade7075acd 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -1469,38 +1469,73 @@ private BoundExpression BuildParamsArray( // if it's available. However, we also disable the optimization if we're in an expression lambda, the // point of which is just to represent the semantics of an operation, and we don't know that all consumers // of expression lambdas will appropriately understand Array.Empty(). - // We disable it for pointer types as well, since they cannot be used as Type Arguments. if (arrayArgs.Length == 0 && !_inExpressionLambda - && paramArrayType is ArrayTypeSymbol ats // could be false if there's a semantic error, e.g. the params parameter type isn't an array - && !ats.ElementType.IsPointerOrFunctionPointer()) + && paramArrayType is ArrayTypeSymbol ats) // could be false if there's a semantic error, e.g. the params parameter type isn't an array { - MethodSymbol? arrayEmpty = _compilation.GetWellKnownTypeMember(WellKnownMember.System_Array__Empty) as MethodSymbol; - if (arrayEmpty != null) // will be null if Array.Empty doesn't exist in reference assemblies + BoundExpression? arrayEmpty = CreateArrayEmptyCallIfAvailable(syntax, ats.ElementType); + if (arrayEmpty is { }) { - _diagnostics.ReportUseSite(arrayEmpty, syntax); - // return an invocation of "Array.Empty()" - arrayEmpty = arrayEmpty.Construct(ImmutableArray.Create(ats.ElementType)); - return new BoundCall( - syntax, - null, - arrayEmpty, - ImmutableArray.Empty, - default(ImmutableArray), - default(ImmutableArray), - isDelegateCall: false, - expanded: false, - invokedAsExtensionMethod: false, - argsToParamsOpt: default(ImmutableArray), - defaultArguments: default(BitVector), - resultKind: LookupResultKind.Viable, - type: arrayEmpty.ReturnType); + return arrayEmpty; } } return CreateParamArrayArgument(syntax, paramArrayType, arrayArgs, _compilation, this); } + private BoundExpression CreateEmptyArray(SyntaxNode syntax, ArrayTypeSymbol arrayType) + { + BoundExpression? arrayEmpty = CreateArrayEmptyCallIfAvailable(syntax, arrayType.ElementType); + if (arrayEmpty is { }) + { + return arrayEmpty; + } + // new T[0] + return new BoundArrayCreation( + syntax, + ImmutableArray.Create( + new BoundLiteral( + syntax, + ConstantValue.Create(0), + _compilation.GetSpecialType(SpecialType.System_Int32))), + initializerOpt: null, + arrayType) + { WasCompilerGenerated = true }; + } + + private BoundExpression? CreateArrayEmptyCallIfAvailable(SyntaxNode syntax, TypeSymbol elementType) + { + if (elementType.IsPointerOrFunctionPointer()) + { + // Pointer types cannot be used as type arguments. + return null; + } + + MethodSymbol? arrayEmpty = _compilation.GetWellKnownTypeMember(WellKnownMember.System_Array__Empty) as MethodSymbol; + if (arrayEmpty is null) // will be null if Array.Empty doesn't exist in reference assemblies + { + return null; + } + + _diagnostics.ReportUseSite(arrayEmpty, syntax); + // return an invocation of "Array.Empty()" + arrayEmpty = arrayEmpty.Construct(ImmutableArray.Create(elementType)); + return new BoundCall( + syntax, + null, + arrayEmpty, + ImmutableArray.Empty, + default(ImmutableArray), + default(ImmutableArray), + isDelegateCall: false, + expanded: false, + invokedAsExtensionMethod: false, + argsToParamsOpt: default(ImmutableArray), + defaultArguments: default(BitVector), + resultKind: LookupResultKind.Viable, + type: arrayEmpty.ReturnType); + } + private static BoundExpression CreateParamArrayArgument(SyntaxNode syntax, TypeSymbol paramArrayType, ImmutableArray arrayArgs, diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs index 69e671205551a..dc0b3bfa36ace 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs @@ -81,23 +81,27 @@ private BoundExpression VisitArrayOrSpanCollectionExpression(BoundCollectionExpr else { int arrayLength = elements.Length; - // https://github.com/dotnet/roslyn/issues/68785: Emit [] as Array.Empty() rather than a List. - var initialization = (arrayLength == 0) - ? null - : new BoundArrayInitialization( - syntax, - isInferred: false, - elements.SelectAsArray(e => VisitExpression(e))); - array = new BoundArrayCreation( - syntax, - ImmutableArray.Create( - new BoundLiteral( + if (arrayLength == 0) + { + array = CreateEmptyArray(syntax, arrayType); + } + else + { + var initialization = new BoundArrayInitialization( syntax, - ConstantValue.Create(arrayLength), - _compilation.GetSpecialType(SpecialType.System_Int32))), - initialization, - arrayType) - { WasCompilerGenerated = true }; + isInferred: false, + elements.SelectAsArray(e => VisitExpression(e))); + array = new BoundArrayCreation( + syntax, + ImmutableArray.Create( + new BoundLiteral( + syntax, + ConstantValue.Create(arrayLength), + _compilation.GetSpecialType(SpecialType.System_Int32))), + initialization, + arrayType) + { WasCompilerGenerated = true }; + } } if (spanConstructor is null) @@ -156,11 +160,31 @@ private BoundExpression VisitCollectionInitializerCollectionExpression(BoundColl private BoundExpression VisitListInterfaceCollectionExpression(BoundCollectionExpression node) { Debug.Assert(!_inExpressionLambda); - Debug.Assert(node.Type is { }); + Debug.Assert(node.Type is NamedTypeSymbol); + + var collectionType = (NamedTypeSymbol)node.Type; + BoundExpression arrayOrList; + + // Use Array.Empty() rather than List for an empty collection expression when + // the target type is IEnumerable, IReadOnlyCollection, or IReadOnlyList. + if (node.Elements.Length == 0 && + collectionType is + { + OriginalDefinition.SpecialType: + SpecialType.System_Collections_Generic_IEnumerable_T or + SpecialType.System_Collections_Generic_IReadOnlyCollection_T or + SpecialType.System_Collections_Generic_IReadOnlyList_T, + TypeArgumentsWithAnnotationsNoUseSiteDiagnostics: [var elementType] + }) + { + arrayOrList = CreateEmptyArray(node.Syntax, ArrayTypeSymbol.CreateSZArray(_compilation.Assembly, elementType)); + } + else + { + arrayOrList = VisitCollectionInitializerCollectionExpression(node, collectionType); + } - // https://github.com/dotnet/roslyn/issues/68785: Emit [] as Array.Empty() rather than a List. - var list = VisitCollectionInitializerCollectionExpression(node, node.Type); - return _factory.Convert(node.Type, list); + return _factory.Convert(collectionType, arrayOrList); } private BoundExpression VisitCollectionBuilderCollectionExpression(BoundCollectionExpression node) diff --git a/src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs b/src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs index ae393210df86d..3829d0d844b9c 100644 --- a/src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs @@ -493,7 +493,7 @@ static void Main() """; CompileAndVerify( new[] { source, s_collectionExtensions }, - expectedOutput: "(System.Collections.Generic.List) [], (System.Collections.Generic.List) [], (System.Collections.Generic.List) [], (System.Collections.Generic.List) [], (System.Collections.Generic.List) [], "); + expectedOutput: "(System.Int32[]) [], (System.Collections.Generic.List) [], (System.Collections.Generic.List) [], (System.Int32[]) [], (System.Int32[]) [], "); } [Fact] @@ -2457,11 +2457,10 @@ static void Main() var verifier = CompileAndVerify(new[] { source, s_collectionExtensions }, expectedOutput: "[], [1, 2], [3, 4, 5], [null, 7], "); verifier.VerifyIL("Program.Create1", """ { - // Code size 7 (0x7) + // Code size 6 (0x6) .maxstack 1 - IL_0000: ldc.i4.0 - IL_0001: newarr "int" - IL_0006: ret + IL_0000: call "int[] System.Array.Empty()" + IL_0005: ret } """); verifier.VerifyIL("Program.Create2", """ @@ -2548,25 +2547,23 @@ static void Report(T[][] a) """); verifier.VerifyIL("Program.Create1", """ { - // Code size 7 (0x7) + // Code size 6 (0x6) .maxstack 1 - IL_0000: ldc.i4.0 - IL_0001: newarr "int[]" - IL_0006: ret + IL_0000: call "int[][] System.Array.Empty()" + IL_0005: ret } """); verifier.VerifyIL("Program.Create2", """ { - // Code size 16 (0x10) + // Code size 15 (0xf) .maxstack 4 IL_0000: ldc.i4.1 IL_0001: newarr "object[]" IL_0006: dup IL_0007: ldc.i4.0 - IL_0008: ldc.i4.0 - IL_0009: newarr "object" - IL_000e: stelem.ref - IL_000f: ret + IL_0008: call "object[] System.Array.Empty()" + IL_000d: stelem.ref + IL_000e: ret } """); verifier.VerifyIL("Program.Create3", """ @@ -2695,12 +2692,11 @@ static void Main() expectedOutput: IncludeExpectedOutput("[], [1, 2], [3, 4, 5], [null, 7], ")); verifier.VerifyIL("Program.Create1", $$""" { - // Code size 12 (0xc) + // Code size 11 (0xb) .maxstack 1 - IL_0000: ldc.i4.0 - IL_0001: newarr "int" - IL_0006: newobj "System.{{spanType}}..ctor(int[])" - IL_000b: ret + IL_0000: call "int[] System.Array.Empty()" + IL_0005: newobj "System.{{spanType}}..ctor(int[])" + IL_000a: ret } """); verifier.VerifyIL("Program.Create2", $$""" @@ -3902,39 +3898,38 @@ static void Main() var verifier = CompileAndVerify(new[] { source, s_collectionExtensions }, expectedOutput: "[1, 2, 3], "); verifier.VerifyIL("Program.Main", """ { - // Code size 62 (0x3e) + // Code size 61 (0x3d) .maxstack 5 .locals init (C V_0) IL_0000: newobj "C..ctor()" IL_0005: stloc.0 IL_0006: ldloc.0 - IL_0007: ldc.i4.0 - IL_0008: newarr "int" - IL_000d: callvirt "void C.Add(params int[])" - IL_0012: ldloc.0 - IL_0013: ldc.i4.2 - IL_0014: newarr "int" - IL_0019: dup - IL_001a: ldc.i4.0 - IL_001b: ldc.i4.1 - IL_001c: stelem.i4 - IL_001d: dup - IL_001e: ldc.i4.1 - IL_001f: ldc.i4.2 - IL_0020: stelem.i4 - IL_0021: callvirt "void C.Add(params int[])" - IL_0026: ldloc.0 - IL_0027: ldc.i4.1 - IL_0028: newarr "int" - IL_002d: dup - IL_002e: ldc.i4.0 - IL_002f: ldc.i4.3 - IL_0030: stelem.i4 - IL_0031: callvirt "void C.Add(params int[])" - IL_0036: ldloc.0 - IL_0037: ldc.i4.0 - IL_0038: call "void CollectionExtensions.Report(object, bool)" - IL_003d: ret + IL_0007: call "int[] System.Array.Empty()" + IL_000c: callvirt "void C.Add(params int[])" + IL_0011: ldloc.0 + IL_0012: ldc.i4.2 + IL_0013: newarr "int" + IL_0018: dup + IL_0019: ldc.i4.0 + IL_001a: ldc.i4.1 + IL_001b: stelem.i4 + IL_001c: dup + IL_001d: ldc.i4.1 + IL_001e: ldc.i4.2 + IL_001f: stelem.i4 + IL_0020: callvirt "void C.Add(params int[])" + IL_0025: ldloc.0 + IL_0026: ldc.i4.1 + IL_0027: newarr "int" + IL_002c: dup + IL_002d: ldc.i4.0 + IL_002e: ldc.i4.3 + IL_002f: stelem.i4 + IL_0030: callvirt "void C.Add(params int[])" + IL_0035: ldloc.0 + IL_0036: ldc.i4.0 + IL_0037: call "void CollectionExtensions.Report(object, bool)" + IL_003c: ret } """); } @@ -5188,6 +5183,244 @@ static void Main() Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "[..e]").WithArguments("System.Collections.Generic.List`1", "ToArray").WithLocation(10, 13)); } + [CombinatorialData] + [Theory] + public void ArrayEmpty_01([CombinatorialValues(TargetFramework.Mscorlib45Extended, TargetFramework.Net80)] TargetFramework targetFramework) + { + if (!ExecutionConditionUtil.IsCoreClr && targetFramework == TargetFramework.Net80) return; + + string source = """ + using System.Collections.Generic; + class Program + { + static void Main() + { + EmptyArray().Report(); + EmptyIEnumerable().Report(); + EmptyICollection().Report(); + EmptyIList().Report(); + EmptyIReadOnlyCollection().Report(); + EmptyIReadOnlyList().Report(); + } + static T[] EmptyArray() => []; + static IEnumerable EmptyIEnumerable() => []; + static ICollection EmptyICollection() => []; + static IList EmptyIList() => []; + static IReadOnlyCollection EmptyIReadOnlyCollection() => []; + static IReadOnlyList EmptyIReadOnlyList() => []; + } + """; + var verifier = CompileAndVerify( + new[] { source, s_collectionExtensions }, + targetFramework: targetFramework, + expectedOutput: "[], [], [], [], [], [], "); + + string expectedIL = (targetFramework == TargetFramework.Mscorlib45Extended) ? + """ + { + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.0 + IL_0001: newarr "T" + IL_0006: ret + } + """ : + """ + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: call "T[] System.Array.Empty()" + IL_0005: ret + } + """; + verifier.VerifyIL("Program.EmptyArray", expectedIL); + verifier.VerifyIL("Program.EmptyIEnumerable", expectedIL); + verifier.VerifyIL("Program.EmptyIReadOnlyCollection", expectedIL); + verifier.VerifyIL("Program.EmptyIReadOnlyList", expectedIL); + + expectedIL = + """ + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: newobj "System.Collections.Generic.List..ctor()" + IL_0005: ret + } + """; + verifier.VerifyIL("Program.EmptyICollection", expectedIL); + verifier.VerifyIL("Program.EmptyIList", expectedIL); + } + + [CombinatorialData] + [Theory] + public void ArrayEmpty_02([CombinatorialValues(TargetFramework.Mscorlib45Extended, TargetFramework.Net80)] TargetFramework targetFramework) + { + if (!ExecutionConditionUtil.IsCoreClr && targetFramework == TargetFramework.Net80) return; + + string source = """ + using System.Collections.Generic; + class Program + { + static void Main() + { + EmptyArray().Report(); + EmptyIEnumerable().Report(); + EmptyICollection().Report(); + EmptyIList().Report(); + EmptyIReadOnlyCollection().Report(); + EmptyIReadOnlyList().Report(); + } + static string[] EmptyArray() => []; + static IEnumerable EmptyIEnumerable() => []; + static ICollection EmptyICollection() => []; + static IList EmptyIList() => []; + static IReadOnlyCollection EmptyIReadOnlyCollection() => []; + static IReadOnlyList EmptyIReadOnlyList() => []; + } + """; + var verifier = CompileAndVerify( + new[] { source, s_collectionExtensions }, + targetFramework: targetFramework, + expectedOutput: "[], [], [], [], [], [], "); + + string expectedIL = (targetFramework == TargetFramework.Mscorlib45Extended) ? + """ + { + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.0 + IL_0001: newarr "string" + IL_0006: ret + } + """ : + """ + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: call "string[] System.Array.Empty()" + IL_0005: ret + } + """; + verifier.VerifyIL("Program.EmptyArray", expectedIL); + verifier.VerifyIL("Program.EmptyIEnumerable", expectedIL); + verifier.VerifyIL("Program.EmptyIReadOnlyCollection", expectedIL); + verifier.VerifyIL("Program.EmptyIReadOnlyList", expectedIL); + + expectedIL = + """ + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: newobj "System.Collections.Generic.List..ctor()" + IL_0005: ret + } + """; + verifier.VerifyIL("Program.EmptyICollection", expectedIL); + verifier.VerifyIL("Program.EmptyIList", expectedIL); + } + + [Fact] + public void ArrayEmpty_PointerElementType() + { + string source = """ + unsafe class Program + { + static void Main() + { + EmptyArray().Report(); + EmptyNestedArray().Report(); + } + static void*[] EmptyArray() => []; + static void*[][] EmptyNestedArray() => []; + } + """; + var verifier = CompileAndVerify( + new[] { source, s_collectionExtensions }, + options: TestOptions.UnsafeReleaseExe, + verify: Verification.FailsPEVerify, + expectedOutput: "[], [], "); + verifier.VerifyIL("Program.EmptyArray", + """ + { + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.0 + IL_0001: newarr "void*" + IL_0006: ret + } + """); + verifier.VerifyIL("Program.EmptyNestedArray", + """ + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: call "void*[][] System.Array.Empty()" + IL_0005: ret + } + """); + } + + [Fact] + public void ArrayEmpty_MissingMethod() + { + string source = """ + using System.Collections.Generic; + class Program + { + static void Main() + { + int[] x = []; + IEnumerable y = []; + x.Report(); + y.Report(); + } + } + """; + + var comp = CreateCompilation(new[] { source, s_collectionExtensions }, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "[], [], "); + verifier.VerifyIL("Program.Main", + """ + { + // Code size 25 (0x19) + .maxstack 2 + .locals init (System.Collections.Generic.IEnumerable V_0) //y + IL_0000: call "int[] System.Array.Empty()" + IL_0005: call "int[] System.Array.Empty()" + IL_000a: stloc.0 + IL_000b: ldc.i4.0 + IL_000c: call "void CollectionExtensions.Report(object, bool)" + IL_0011: ldloc.0 + IL_0012: ldc.i4.0 + IL_0013: call "void CollectionExtensions.Report(object, bool)" + IL_0018: ret + } + """); + + comp = CreateCompilation(new[] { source, s_collectionExtensions }, options: TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_Array__Empty); + verifier = CompileAndVerify(comp, expectedOutput: "[], [], "); + verifier.VerifyIL("Program.Main", + """ + { + // Code size 27 (0x1b) + .maxstack 3 + .locals init (int[] V_0) //x + IL_0000: ldc.i4.0 + IL_0001: newarr "int" + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: newarr "int" + IL_000d: ldloc.0 + IL_000e: ldc.i4.0 + IL_000f: call "void CollectionExtensions.Report(object, bool)" + IL_0014: ldc.i4.0 + IL_0015: call "void CollectionExtensions.Report(object, bool)" + IL_001a: ret + } + """); + } + [Fact] public void Nullable_01() { @@ -5736,13 +5969,12 @@ static MyCollection F2(int x, object y) verifier.VerifyIL("Program.F0", """ { - // Code size 17 (0x11) + // Code size 16 (0x10) .maxstack 1 - IL_0000: ldc.i4.0 - IL_0001: newarr "string" - IL_0006: newobj "System.ReadOnlySpan..ctor(string[])" - IL_000b: call "MyCollection MyCollectionBuilder.Create(System.ReadOnlySpan)" - IL_0010: ret + IL_0000: call "string[] System.Array.Empty()" + IL_0005: newobj "System.ReadOnlySpan..ctor(string[])" + IL_000a: call "MyCollection MyCollectionBuilder.Create(System.ReadOnlySpan)" + IL_000f: ret } """); verifier.VerifyIL("Program.F1",