|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.CodeGen
{
internal partial class CodeGenerator
{
private enum ArrayInitializerStyle
{
// Initialize every element
Element,
// Initialize all elements at once from a metadata blob
Block,
// Mixed case where there are some initializers that are constants and
// there is enough of them so that it makes sense to use block initialization
// followed by individual initialization of non-constant elements
Mixed,
}
/// <summary>
/// Entry point to the array initialization.
/// Assumes that we have newly created array on the stack.
///
/// inits could be an array of values for a single dimensional array
/// or an array (of array)+ of values for a multidimensional case
///
/// in either case it is expected that number of leaf values will match number
/// of elements in the array and nesting level should match the rank of the array.
/// </summary>
private void EmitArrayInitializers(ArrayTypeSymbol arrayType, BoundArrayInitialization inits)
{
var initExprs = inits.Initializers;
var initializationStyle = ShouldEmitBlockInitializer(arrayType.ElementType, initExprs);
if (initializationStyle == ArrayInitializerStyle.Element)
{
this.EmitElementInitializers(arrayType, initExprs, true);
}
else
{
ImmutableArray<byte> data = this.GetRawData(initExprs);
_builder.EmitArrayBlockInitializer(data, inits.Syntax, _diagnostics.DiagnosticBag);
if (initializationStyle == ArrayInitializerStyle.Mixed)
{
EmitElementInitializers(arrayType, initExprs, false);
}
}
}
private void EmitElementInitializers(ArrayTypeSymbol arrayType,
ImmutableArray<BoundExpression> inits,
bool includeConstants)
{
if (!IsMultidimensionalInitializer(inits))
{
EmitVectorElementInitializers(arrayType, inits, includeConstants);
}
else
{
EmitMultidimensionalElementInitializers(arrayType, inits, includeConstants);
}
}
private void EmitVectorElementInitializers(ArrayTypeSymbol arrayType,
ImmutableArray<BoundExpression> inits,
bool includeConstants)
{
for (int i = 0; i < inits.Length; i++)
{
var init = inits[i];
if (ShouldEmitInitExpression(includeConstants, init))
{
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitIntConstant(i);
EmitExpression(init, true);
EmitVectorElementStore(arrayType, init.Syntax);
}
}
}
// if element init is not a constant we have no choice - we need to emit it
// if element is a default value - no need to emit initializer, arrays are created zero inited.
// if element is a not a constant or includeConstants flag is set, return true
private static bool ShouldEmitInitExpression(bool includeConstants, BoundExpression init)
{
if (init.IsDefaultValue())
{
return false;
}
return includeConstants || init.ConstantValueOpt == null;
}
/// <summary>
/// To handle array initialization of arbitrary rank it is convenient to
/// approach multidimensional initialization as a recursively nested.
///
/// ForAll{i, j, k} Init(i, j, k) ===>
/// ForAll{i} ForAll{j, k} Init(i, j, k) ===>
/// ForAll{i} ForAll{j} ForAll{k} Init(i, j, k)
///
/// This structure is used for capturing initializers of a given index and
/// the index value itself.
/// </summary>
private readonly struct IndexDesc
{
public IndexDesc(int index, ImmutableArray<BoundExpression> initializers)
{
this.Index = index;
this.Initializers = initializers;
}
public readonly int Index;
public readonly ImmutableArray<BoundExpression> Initializers;
}
private void EmitMultidimensionalElementInitializers(ArrayTypeSymbol arrayType,
ImmutableArray<BoundExpression> inits,
bool includeConstants)
{
// Using a List for the stack instead of the framework Stack because IEnumerable from Stack is top to bottom.
// This algorithm requires the IEnumerable to be from bottom to top. See extensions for List in CollectionExtensions.vb.
var indices = new ArrayBuilder<IndexDesc>();
// emit initializers for all values of the leftmost index.
for (int i = 0; i < inits.Length; i++)
{
indices.Push(new IndexDesc(i, ((BoundArrayInitialization)inits[i]).Initializers));
EmitAllElementInitializersRecursive(arrayType, indices, includeConstants);
}
Debug.Assert(!indices.Any());
}
/// <summary>
/// Emits all initializers that match indices on the stack recursively.
///
/// Example:
/// if array has [0..2, 0..3, 0..2] shape
/// and we have {1, 2} indices on the stack
/// initializers for
/// [1, 2, 0]
/// [1, 2, 1]
/// [1, 2, 2]
///
/// will be emitted and the top index will be pushed off the stack
/// as at that point we would be completely done with emitting initializers
/// corresponding to that index.
/// </summary>
private void EmitAllElementInitializersRecursive(ArrayTypeSymbol arrayType,
ArrayBuilder<IndexDesc> indices,
bool includeConstants)
{
var top = indices.Peek();
var inits = top.Initializers;
if (IsMultidimensionalInitializer(inits))
{
// emit initializers for the less significant indices recursively
for (int i = 0; i < inits.Length; i++)
{
indices.Push(new IndexDesc(i, ((BoundArrayInitialization)inits[i]).Initializers));
EmitAllElementInitializersRecursive(arrayType, indices, includeConstants);
}
}
else
{
// leaf case
for (int i = 0; i < inits.Length; i++)
{
var init = inits[i];
if (ShouldEmitInitExpression(includeConstants, init))
{
// emit array ref
_builder.EmitOpCode(ILOpCode.Dup);
Debug.Assert(indices.Count == arrayType.Rank - 1);
// emit values of all indices that are in progress
foreach (var row in indices)
{
_builder.EmitIntConstant(row.Index);
}
// emit the leaf index
_builder.EmitIntConstant(i);
var initExpr = inits[i];
EmitExpression(initExpr, true);
EmitArrayElementStore(arrayType, init.Syntax);
}
}
}
indices.Pop();
}
private static ConstantValue AsConstOrDefault(BoundExpression init)
{
ConstantValue initConstantValueOpt = init.ConstantValueOpt;
if (initConstantValueOpt != null)
{
return initConstantValueOpt;
}
TypeSymbol type = init.Type.EnumUnderlyingTypeOrSelf();
return ConstantValue.Default(type.SpecialType);
}
/// <summary>
/// Determine if enum arrays can be initialized using block initialization.
/// </summary>
/// <returns>True if it's safe to use block initialization for enum arrays.</returns>
/// <remarks>
/// In NetFx 4.0, block array initializers do not work on all combinations of {32/64 X Debug/Retail} when array elements are enums.
/// This is fixed in 4.5 thus enabling block array initialization for a very common case.
/// We look for the presence of <see cref="System.Runtime.GCLatencyMode.SustainedLowLatency"/> which was introduced in .NET Framework 4.5
/// </remarks>
private bool EnableEnumArrayBlockInitialization
{
get
{
return _module.Compilation.EnableEnumArrayBlockInitialization;
}
}
private ArrayInitializerStyle ShouldEmitBlockInitializer(TypeSymbol elementType, ImmutableArray<BoundExpression> inits)
{
if (_module.IsEncDelta)
{
// Avoid using FieldRva table. Can be allowed if tested on all supported runtimes.
// Consider removing: https://github.com/dotnet/roslyn/issues/69480
return ArrayInitializerStyle.Element;
}
if (elementType.IsEnumType())
{
if (!EnableEnumArrayBlockInitialization)
{
return ArrayInitializerStyle.Element;
}
elementType = elementType.EnumUnderlyingTypeOrSelf();
}
if (elementType.SpecialType.IsBlittable())
{
if (_module.GetInitArrayHelper() == null)
{
return ArrayInitializerStyle.Element;
}
int initCount = 0;
int constCount = 0;
InitializerCountRecursive(inits, ref initCount, ref constCount);
if (initCount > 2)
{
if (initCount == constCount)
{
return ArrayInitializerStyle.Block;
}
int thresholdCnt = Math.Max(3, (initCount / 3));
if (constCount >= thresholdCnt)
{
return ArrayInitializerStyle.Mixed;
}
}
}
return ArrayInitializerStyle.Element;
}
/// <summary>
/// Count of all nontrivial initializers and count of those that are constants.
/// </summary>
private void InitializerCountRecursive(ImmutableArray<BoundExpression> inits, ref int initCount, ref int constInits)
{
if (inits.Length == 0)
{
return;
}
foreach (var init in inits)
{
var asArrayInit = init as BoundArrayInitialization;
if (asArrayInit != null)
{
InitializerCountRecursive(asArrayInit.Initializers, ref initCount, ref constInits);
}
else
{
// NOTE: default values do not need to be initialized.
// .NET arrays are always zero-inited.
if (!init.IsDefaultValue())
{
initCount += 1;
if (init.ConstantValueOpt != null)
{
constInits += 1;
}
}
}
}
}
/// <summary>
/// Produces a serialized blob of all constant initializers.
/// Non-constant initializers are matched with a zero of corresponding size.
/// </summary>
private ImmutableArray<byte> GetRawData(ImmutableArray<BoundExpression> initializers)
{
// the initial size is a guess.
// there is no point to be precise here as MemoryStream always has N + 1 storage
// and will need to be trimmed regardless
var writer = new BlobBuilder(initializers.Length * 4);
SerializeArrayRecursive(writer, initializers);
return writer.ToImmutableArray();
}
private void SerializeArrayRecursive(BlobBuilder bw, ImmutableArray<BoundExpression> inits)
{
if (inits.Length != 0)
{
if (inits[0].Kind == BoundKind.ArrayInitialization)
{
foreach (var init in inits)
{
SerializeArrayRecursive(bw, ((BoundArrayInitialization)init).Initializers);
}
}
else
{
foreach (var init in inits)
{
AsConstOrDefault(init).Serialize(bw);
}
}
}
}
/// <summary>
/// Check if it is a regular collection of expressions or there are nested initializers.
/// </summary>
private static bool IsMultidimensionalInitializer(ImmutableArray<BoundExpression> inits)
{
Debug.Assert(inits.All((init) => init.Kind != BoundKind.ArrayInitialization) ||
inits.All((init) => init.Kind == BoundKind.ArrayInitialization),
"all or none should be nested");
return inits.Length != 0 && inits[0].Kind == BoundKind.ArrayInitialization;
}
#nullable enable
/// <summary>Tries to emit a ReadOnlySpan construction as a wrapper for a blob rather than as a wrapper for an array construction.</summary>
/// <param name="spanType">The type of the span being constructed.</param>
/// <param name="wrappedExpression">The expression being wrapped in a span.</param>
/// <param name="used">true if the result of the expression is used; false if it's required only for its side effects.</param>
/// <param name="inPlaceTarget">A non-null expression if the construction is initializing an existing local in-place; otherwise, null.</param>
/// <param name="avoidInPlace">
/// An output Boolean indicating whether a caller trying to perform in-place initialization should instead prefer to assign the local to a new value.
/// Call sites may try to optimize an assignment to a newly-created struct by calling the constructor directly rather than assigning, but that
/// may then inhibit the more valuable optimization of creating a span via RuntimeHelpers.CreateSpan, which needs to assign. When a caller has passed
/// in an <paramref name="inPlaceTarget"/> but CreateSpan could be used if it weren't, this method may return false and set <paramref name="avoidInPlace"/>
/// to true to inform the caller it can try again without the <paramref name="inPlaceTarget"/>.
/// </param>
/// <param name="start">The expression for the offset into the array being wrapped in a span.</param>
/// <param name="length">The expression for the length of the subarray being wrapped in a span.</param>
/// <returns>
/// true if this method successfully emit a ReadOnlySpan as a wrapper for a blob; otherwise, false. If false, nothing will have been emitted.
/// And if false and <paramref name="avoidInPlace"/> is true (in which case <paramref name="inPlaceTarget"/> must have been non-null), the caller
/// may try again but with a null <paramref name="inPlaceTarget"/>.
/// </returns>
private bool TryEmitOptimizedReadonlySpanCreation(NamedTypeSymbol spanType, BoundExpression wrappedExpression, bool used, BoundExpression inPlaceTarget, out bool avoidInPlace, BoundExpression? start = null, BoundExpression? length = null)
{
// The purpose of this optimization is to replace a BoundArrayCreation with better code generation.
// We're looking for an expression like:
// new ReadOnlySpan<T>(new T[] { const, const, ... })
// new ReadOnlySpan<T>(new T[] { const, const, ... }, 0, length)
// (ReadOnlySpan<T>)new T[] { const, const, ... }
// etc., and wrappedExpression is that array creation. For single byte primitives, we can replace that
// with the equivalent of:
// new ReadOnlySpan<T>((void*)PrivateImplementationDetails.DataField, Length)
// on all target platforms. For primitives larger than a single byte, if the target platform exposes
// the RuntimeHelpers.CreateSpan method, we can emit it instead as:
// RuntimeHelpers.CreateSpan(PrivateImplementationDetails.DataFieldToken)
// and for platforms that lack CreateSpan, as a span that wraps a lazily-initialized array:
// new ReadOnlySpan<T>(PrivateImplementationDetails.ArrayField ??= new T[] { ... })
// For non-constant data, unsupported primitive types, and other variations, this optimization will fail
// and the method will return false indicating that no code was emitted.
//
// A pattern like the following is also special-cased via the `inPlaceTarget` parameter:
// ReadOnlySpan<T> span = new ReadOnlySpan<T>(new T[] { const, const, ... });
// Rather than constructing a span and assigning it to the local, the caller of this method
// may try to initialize the local in-place. In that case, this method is responsible for emitting
// a call to the span's constructor. It can do so for some cases, but in cases that
// require the use of RuntimeHelpers.CreateSpan, assignment is a necessity, and as such calls
// requiring in-place construction will fail; the code below may set `avoidInPlace` to true
// indicating the caller can try again not in place and it should succeed.
//
// This optimization is also used as part of emitting UTF8 string literals. The code:
// ReadOnlySpan<byte> span = "abc"u8;
// is lowered to the equivalent of:
// ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { (byte)'a', (byte)'b', (byte)'c', (byte)'\0' }, 0, 3);
// with this optimization then being used to avoid that byte[] allocation. Support for u8
// is also the reason this method accepts a start/length, in order to support trimming
// the null terminator off as part of creating the span instance.
Debug.Assert(inPlaceTarget is null || TargetIsNotOnHeap(inPlaceTarget), "in-place construction target should not be on heap");
RoslynDebug.Assert(_diagnostics.DiagnosticBag is not null, $"Expected non-null {nameof(_diagnostics)}.{nameof(_diagnostics.DiagnosticBag)}");
if (start is null != length is null)
{
// start and length always need to be provided as a pair.
throw ExceptionUtilities.Unreachable();
}
avoidInPlace = false;
SpecialType specialElementType = SpecialType.None;
if (inPlaceTarget is null && !used)
{
// The caller has specified that we're creating a ReadOnlySpan expression that won't be used.
// We needn't emit anything.
return true;
}
if (_module.IsEncDelta)
{
// Avoid using FieldRva table. Can be allowed if tested on all supported runtimes.
// Consider removing: https://github.com/dotnet/roslyn/issues/69480
return false;
}
// The primary optimization here is for byte-sized primitives that can wrap a ReadOnlySpan directly around a pointer
// into a blob. That requires the ReadOnlySpan(void*, int) ctor. If this constructor isn't available, we give up on
// all optimizations. Technically, if this ctor isn't available but the ReadOnlySpan(T[]) constructor is, we could still
// proceed to use the cached array mechanism. But all known ReadOnlySpan implementations have always provided both
// constructors, and it's not worth trying to optimize here for an arbitrary implementation that has a different shape.
var rosPointerCtor = (MethodSymbol?)Binder.GetWellKnownTypeMember(_module.Compilation, WellKnownMember.System_ReadOnlySpan_T__ctor_Pointer, _diagnostics, syntax: wrappedExpression.Syntax, isOptional: true);
if (rosPointerCtor is null)
{
return false;
}
Debug.Assert(!rosPointerCtor.HasUnsupportedMetadata);
ArrayTypeSymbol? arrayType = null;
TypeSymbol? elementType = null;
if (wrappedExpression is not BoundArrayCreation { InitializerOpt: { } initializer } ac)
{
return false;
}
// Get the array type and its element type.
arrayType = (ArrayTypeSymbol)ac.Type;
elementType = arrayType.ElementType;
ImmutableArray<BoundExpression> initializers = initializer.Initializers;
var elementCount = initializers.Length;
if (elementCount == 0)
{
emitEmptyReadonlySpan(spanType, wrappedExpression, used, inPlaceTarget);
return true;
}
if (initializers.Any(static init => init.ConstantValueOpt == null))
{
return false;
}
// The blob optimization is only supported for core primitive types that can be stored in metadata blobs.
// For enums, we need to use the underlying type.
specialElementType = elementType.EnumUnderlyingTypeOrSelf().SpecialType;
if (!IsTypeAllowedInBlobWrapper(specialElementType))
{
return start is null && length is null
&& tryEmitAsCachedArrayOfConstants(ac, arrayType, elementType, spanType, used, inPlaceTarget, out avoidInPlace);
}
if (IsPeVerifyCompatEnabled())
{
// After this point, we're emitting code that may cause PEVerify to warn, so stop if PEVerify compat is enabled.
return false;
}
// Get the data and number of elements that compose the initialization.
ImmutableArray<byte> data = GetRawDataForArrayInit(initializers);
Debug.Assert(arrayType is not null);
Debug.Assert(elementType is not null);
int lengthForConstructor;
if (start is not null)
{
// The start expression needs to be 0.
if (start.ConstantValueOpt?.IsDefaultValue != true || start.ConstantValueOpt.Discriminator != ConstantValueTypeDiscriminator.Int32)
{
return false;
}
// The length expression needs to be an Int32, and it needs to be in the range [0, elementCount].
Debug.Assert(length is not null);
if (length.ConstantValueOpt?.Discriminator != ConstantValueTypeDiscriminator.Int32)
{
return false;
}
lengthForConstructor = length.ConstantValueOpt.Int32Value;
if (lengthForConstructor > elementCount || lengthForConstructor < 0)
{
return false;
}
}
else
{
// There's no start/length, so the length to use with a constructor is the element count.
lengthForConstructor = elementCount;
}
if (specialElementType.SizeInBytes() == 1)
{
// We're dealing with a ReadOnlySpan<byte/sbyte/bool>. We can optimize this on all target platforms,
// whether the initialization is in-place or not.
if (inPlaceTarget is not null)
{
EmitAddress(inPlaceTarget, AddressKind.Writeable);
}
// Map a field to the block (that makes it addressable).
var field = _builder.module.GetFieldForData(data, alignment: 1, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Ldsflda);
_builder.EmitToken(field, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitIntConstant(lengthForConstructor);
if (inPlaceTarget is not null)
{
// Consumes target ref, data ptr and size, pushes nothing.
_builder.EmitOpCode(ILOpCode.Call, stackAdjustment: -3);
}
else
{
// Consumes data ptr and size, pushes the instance.
Debug.Assert(used);
_builder.EmitOpCode(ILOpCode.Newobj, stackAdjustment: -1);
}
EmitSymbolToken(rosPointerCtor.AsMember(spanType), wrappedExpression.Syntax, optArgList: null);
if (inPlaceTarget is not null && used)
{
EmitExpression(inPlaceTarget, used: true);
}
return true;
}
// We're dealing with a primitive that's larger than a single byte.
Debug.Assert(specialElementType.SizeInBytes() is 2 or 4 or 8, "Supported primitives are expected to be 2, 4, or 8 bytes");
if (lengthForConstructor != elementCount)
{
// We need to use RuntimeHelpers.CreateSpan / cached array, but the code has requested a subset of the elements.
// That means the code is something like `new ReadOnlySpan<char>(new[] { 'a', 'b', 'c' }, 1, 2)`
// rather than `new ReadOnlySpan<char>(new[] { 'b', 'c' })`. If such a pattern is found to be
// common, this could be augmented to accommodate it. For now, we just return false to fail
// to optimize this case.
return false;
}
if (inPlaceTarget is not null)
{
// We can use RuntimeHelpers.CreateSpan, but not for in-place initialization. Fail to optimize,
// but tell the caller they can call this again with a null inPlaceTarget, at which point this
// should be able to optimize the call.
avoidInPlace = true;
return false;
}
// As we're dealing with multi-byte types, endianness needs to be considered. Such handling is provided by the
// runtime's RuntimeHelpers.CreateSpan, which will wrap a span around the blob on little endian and which will
// allocate an array and cache it on big endian.
MethodSymbol? createSpan = (MethodSymbol?)Binder.GetWellKnownTypeMember(_module.Compilation, WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__CreateSpanRuntimeFieldHandle, _diagnostics, syntax: wrappedExpression.Syntax, isOptional: true);
if (createSpan is not null)
{
// CreateSpan was available. Use it.
Debug.Assert(!createSpan.HasUnsupportedMetadata);
// ldtoken <PrivateImplementationDetails>...
// call ReadOnlySpan<elementType> RuntimeHelpers::CreateSpan<elementType>(fldHandle)
var field = _builder.module.GetFieldForData(data, alignment: (ushort)specialElementType.SizeInBytes(), wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Ldtoken);
_builder.EmitToken(field, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Call, stackAdjustment: 0);
EmitSymbolToken(createSpan.Construct(elementType), wrappedExpression.Syntax, optArgList: null);
return true;
}
// We're dealing with a multi-byte primitive, and CreateSpan was not available. Get a static field from PrivateImplementationDetails,
// and use it as a lazily-initialized cache for an array for this data:
// new ReadOnlySpan<T>(PrivateImplementationDetails.ArrayField ??= RuntimeHelpers.InitializeArray(new int[Length], PrivateImplementationDetails.DataField));
return tryEmitAsCachedArrayFromBlob(spanType, wrappedExpression, elementCount, data, ref arrayType, elementType);
// Emit: new ReadOnlySpan<T>(PrivateImplementationDetails.ArrayField ??= RuntimeHelpers.InitializeArray(new int[Length], PrivateImplementationDetails.DataField));
bool tryEmitAsCachedArrayFromBlob(NamedTypeSymbol spanType, BoundExpression wrappedExpression, int elementCount, ImmutableArray<byte> data, ref ArrayTypeSymbol arrayType, TypeSymbol elementType)
{
if (!tryGetReadOnlySpanArrayCtor(wrappedExpression.Syntax, out var rosArrayCtor))
{
return false;
}
// If we're dealing with an array of enums, we need to handle the possibility that the data blob
// is the same for multiple enums all with the same underlying type, or even with the underlying type
// itself. This is addressed by always caching an array for the underlying type, and then relying on
// arrays being covariant between the underlying type and the enum type, so that it's safe to do:
// new ReadOnlySpan<EnumType>(arrayOfUnderlyingType);
// It's important to have a consistent type here, as otherwise the type of the caching field could
// end up changing non-deterministically based on which type for a given blob was encountered first.
// Also, even if we're not dealing with an enum, we still create a new array type that drops any
// annotations that may have initially been associated with the element type; this is similarly to
// ensure deterministic behavior.
arrayType = arrayType.WithElementType(TypeWithAnnotations.Create(elementType.EnumUnderlyingTypeOrSelf()));
var cachingField = _builder.module.GetArrayCachingFieldForData(data, _module.Translate(arrayType), wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
var arrayNotNullLabel = new object();
// T[]? array = PrivateImplementationDetails.cachingField;
// if (array is not null) goto arrayNotNull;
_builder.EmitOpCode(ILOpCode.Ldsfld);
_builder.EmitToken(cachingField, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitBranch(ILOpCode.Brtrue, arrayNotNullLabel);
// array = new T[elementCount];
// RuntimeHelpers.InitializeArray(token, array);
// PrivateImplementationDetails.cachingField = array;
_builder.EmitOpCode(ILOpCode.Pop);
_builder.EmitIntConstant(elementCount);
_builder.EmitOpCode(ILOpCode.Newarr);
EmitSymbolToken(arrayType.ElementType, wrappedExpression.Syntax);
_builder.EmitArrayBlockInitializer(data, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitOpCode(ILOpCode.Stsfld);
_builder.EmitToken(cachingField, wrappedExpression.Syntax, _diagnostics.DiagnosticBag);
// arrayNotNullLabel:
// new ReadOnlySpan<T>(array)
_builder.MarkLabel(arrayNotNullLabel);
_builder.EmitOpCode(ILOpCode.Newobj, 0);
EmitSymbolToken(rosArrayCtor.AsMember(spanType), wrappedExpression.Syntax, optArgList: null);
return true;
}
// Emit: new ReadOnlySpan<ElementType>(PrivateImplementationDetails.cachingField ??= new ElementType[] { ... constants ... })
bool tryEmitAsCachedArrayOfConstants(BoundArrayCreation arrayCreation, ArrayTypeSymbol arrayType, TypeSymbol elementType, NamedTypeSymbol spanType, bool used, BoundExpression? inPlaceTarget, out bool avoidInPlace)
{
avoidInPlace = false;
if (elementType.IsReferenceType && elementType.SpecialType != SpecialType.System_String)
{
return false;
}
var initializer = arrayCreation.InitializerOpt;
Debug.Assert(initializer != null);
var initializers = initializer.Initializers;
Debug.Assert(initializers.All(static init => init.ConstantValueOpt != null));
Debug.Assert(!elementType.IsEnumType());
if (!tryGetReadOnlySpanArrayCtor(arrayCreation.Syntax, out var rosArrayCtor))
{
return false;
}
if (inPlaceTarget is not null)
{
EmitAddress(inPlaceTarget, AddressKind.Writeable);
}
ImmutableArray<ConstantValue> constants = initializers.SelectAsArray(static init => init.ConstantValueOpt!);
Cci.IFieldReference cachingField = _builder.module.GetArrayCachingFieldForConstants(constants, _module.Translate(arrayType),
arrayCreation.Syntax, _diagnostics.DiagnosticBag);
var arrayNotNullLabel = new object();
// T[]? array = PrivateImplementationDetails.cachingField;
// if (array is not null) goto arrayNotNull;
_builder.EmitOpCode(ILOpCode.Ldsfld);
_builder.EmitToken(cachingField, arrayCreation.Syntax, _diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitBranch(ILOpCode.Brtrue, arrayNotNullLabel);
// array = arrayCreation;
// PrivateImplementationDetails.cachingField = array;
_builder.EmitOpCode(ILOpCode.Pop);
EmitExpression(arrayCreation, used: true);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitOpCode(ILOpCode.Stsfld);
_builder.EmitToken(cachingField, arrayCreation.Syntax, _diagnostics.DiagnosticBag);
// arrayNotNullLabel:
// new ReadOnlySpan<T>(array)
_builder.MarkLabel(arrayNotNullLabel);
if (inPlaceTarget is not null)
{
// Consumes target ref, array, pushes nothing.
_builder.EmitOpCode(ILOpCode.Call, stackAdjustment: -2);
}
else
{
// Consumes array, pushes the instance.
Debug.Assert(used);
_builder.EmitOpCode(ILOpCode.Newobj, stackAdjustment: 0);
}
EmitSymbolToken(rosArrayCtor.AsMember(spanType), arrayCreation.Syntax, optArgList: null);
if (inPlaceTarget is not null && used)
{
EmitExpression(inPlaceTarget, used: true);
}
return true;
}
// The span is empty. Optimize away the array. This works regardless of the size of the type.
void emitEmptyReadonlySpan(NamedTypeSymbol spanType, BoundExpression wrappedExpression, bool used, BoundExpression? inPlaceTarget)
{
// If this is in-place initialization, call the default ctor.
if (inPlaceTarget is not null)
{
EmitAddress(inPlaceTarget, AddressKind.Writeable);
_builder.EmitOpCode(ILOpCode.Initobj);
EmitSymbolToken(spanType, wrappedExpression.Syntax);
if (used)
{
EmitExpression(inPlaceTarget, used: true);
}
}
else
{
// Otherwise, assign it to a default value / empty span.
Debug.Assert(used);
EmitDefaultValue(spanType, used, wrappedExpression.Syntax);
}
}
bool tryGetReadOnlySpanArrayCtor(SyntaxNode syntax, [NotNullWhen(true)] out MethodSymbol? rosArrayCtor)
{
rosArrayCtor = (MethodSymbol?)Binder.GetWellKnownTypeMember(_module.Compilation, WellKnownMember.System_ReadOnlySpan_T__ctor_Array, _diagnostics, syntax: syntax, isOptional: true);
if (rosArrayCtor is null)
{
// The ReadOnlySpan<T>(T[] array) constructor we need is missing or something went wrong.
return false;
}
Debug.Assert(!rosArrayCtor.HasUnsupportedMetadata);
return true;
}
}
/// <summary>Gets whether the element type of an array is appropriate for storing in a blob.</summary>
internal static bool IsTypeAllowedInBlobWrapper(SpecialType type) => type is
// 1 byte
// For primitives that are a single byte in size, a span can point directly to a blob
// containing the constant data.
SpecialType.System_SByte or SpecialType.System_Byte or SpecialType.System_Boolean or
// For primitives that are > 1 byte in size, we can either use CreateSpan if it's available
// or fall back to caching an array.
// 2 bytes
SpecialType.System_Int16 or SpecialType.System_UInt16 or SpecialType.System_Char or
// 4 bytes
SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Single or
// 8 bytes
SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Double;
/// <summary>
/// Returns a byte blob that matches serialized content of single array initializer of constants.
/// </summary>
private ImmutableArray<byte> GetRawDataForArrayInit(ImmutableArray<BoundExpression> initializers)
{
Debug.Assert(initializers.Length > 0);
Debug.Assert(initializers.All(static init => init.ConstantValueOpt != null));
var writer = new BlobBuilder(initializers.Length * 4);
foreach (var init in initializers)
{
init.ConstantValueOpt!.Serialize(writer);
}
return writer.ToImmutableArray();
}
}
}
|