Skip to content

Commit

Permalink
change naming of field in PrivateImplementationDetails nested type to…
Browse files Browse the repository at this point in the history
… match Roslyn naming (#324)
  • Loading branch information
adrianoc committed Jan 11, 2025
1 parent 9f906d4 commit 749380e
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Cecilifier.Core.Tests/Tests/Unit/ArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public void InitializationOptimized(string code)
\s+\1.NestedTypes.Add\(\2\);
\s+var (fld_arrayInitializerData_\d+) = new FieldDefinition\("[A-Z0-9]+", FieldAttributes.Assembly \| FieldAttributes.Static \| FieldAttributes.InitOnly, \2\);
\s+\1.Fields.Add\(\3\);
\s+\3.InitialValue = Cecilifier.Runtime.TypeHelpers.ToByteArray<Int32>\(new Int32\[\] { 1, 2, 3 }\);
\s+\3.InitialValue = \[ 0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00, \];
\s+(il_.+).Emit\(OpCodes.Dup\);
\s+\4.Emit\(OpCodes.Ldtoken, \3\);
"""));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using Cecilifier.Core.CodeGeneration;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Misc;
using Cecilifier.Core.Variables;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Tests.Unit;
Expand All @@ -20,12 +26,12 @@ public void PrivateImplementationType_IsCached()
var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Any(), Is.False);

var _ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 10, "int", "123");
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Count(), Is.EqualTo(2), "2 types should have been generated. PrivateImplementationDetails and a second one, used to store the raw data");

// run a second time... simulating a second array initialization being processed.
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 10,"int","123");
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Count(), Is.EqualTo(2));
}
Expand All @@ -39,34 +45,56 @@ public void Int32AndInt64_AreUsedAsFieldBackingType_OfArraysOf4And8Bytes()
var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Any(), Is.False);

var _ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 4, "int", "0123");
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(byte), ["1", "2", "3", "4"], StringToSpanOfBytesConverters.Byte);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Count(), Is.EqualTo(1));
Assert.That(context.Output, Does.Match(@"var fld_arrayInitializerData_1 = new FieldDefinition\(.+assembly.MainModule.TypeSystem.Int32\);"));

// run a second time... simulating a second array initialization being processed.
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 8,"int","012345678");
_ = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(short), ["1", "2", "3", "4"], StringToSpanOfBytesConverters.Int16);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Type);
Assert.That(found.Count(), Is.EqualTo(1));

Assert.That(context.Output, Does.Match(@"var fld_arrayInitializerData_2 = new FieldDefinition\(.+assembly.MainModule.TypeSystem.Int64\);"));
}

[TestCaseSource(nameof(BackingFieldNameTestScenarios))]
public void BackingFieldName_Matches_CSharpCompilerNaming(string array, int elementSize, string expectedFieldName)
{
// If this test every fail it has a high chance that a new version of Roslyn has changed the way the field name is computed.
// This test assumes the implementation from: https://github.com/dotnet/roslyn/blob/b7e891b8a884be1519a709edc7121140c5a1fac2/src/Compilers/Core/Portable/CodeGen/PrivateImplementationDetails.cs#L209
var comp = CompilationFor($"class Foo {{ {array} }}");
var context = new CecilifierContext(comp.GetSemanticModel(comp.SyntaxTrees[0]), new CecilifierOptions(), 1);

var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field).SingleOrDefault();
Assert.That(found, Is.Null);

var arrayDeclaration = comp.SyntaxTrees[0].GetRoot().DescendantNodes().OfType<VariableDeclarationSyntax>().Single();
var stringToByteSpanConverter = StringToSpanOfBytesConverters.For(arrayDeclaration.Type.NameFrom());
var elements = comp.SyntaxTrees[0].GetRoot().DescendantNodes().OfType<LiteralExpressionSyntax>().Select(exp => exp.ValueText()).ToArray();

PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, elementSize, elements, stringToByteSpanConverter);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field).SingleOrDefault();
Assert.That(found, Is.Not.Null);

Assert.That(found.MemberName, Is.EqualTo(expectedFieldName), array);
}

[Test]
public void BackingField_ForSameSize_IsCached()
{
var comp = CompilationFor("class Foo {}");
var comp = CompilationFor("class Foo {}");
var context = new CecilifierContext(comp.GetSemanticModel(comp.SyntaxTrees[0]), new CecilifierOptions(), 1);

var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);
Assert.That(found.Any(), Is.False);

var variableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 10, "int", "123");
var variableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);
Assert.That(found.Count(), Is.EqualTo(1));

// run a second time... simulating a second array initialization with same size being processed.
var secondVariableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 10, "int","123");
var secondVariableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);

Assert.That(found.Count(), Is.EqualTo(1));
Expand All @@ -82,12 +110,12 @@ public void BackingField_IsUniquePerDataSize()
var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);
Assert.That(found.Any(), Is.False);

var variableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 12, "int","{1, 2, 3, 4}");
var variableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);
Assert.That(found.Count(), Is.EqualTo(1));

// run a second time... simulating a second array initialization with a different size being processed.
var secondVariableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, 20, "int","{1, 2 , 3, 4, 5}");
var secondVariableName = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(context, sizeof(int), ["1", "2", "3", "4"], StringToSpanOfBytesConverters.Int32);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Field);

Assert.That(found.Count(), Is.EqualTo(2), context.Output);
Expand Down Expand Up @@ -123,4 +151,53 @@ static CSharpCompilation CompilationFor(string code)
var syntaxTree = CSharpSyntaxTree.ParseText(code);
return CSharpCompilation.Create("Test", new[] { syntaxTree }, references: new [] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), });
}

static TestCaseData[] BackingFieldNameTestScenarios()
{
var actualPrivateImplementationDetails = Type.GetType("<PrivateImplementationDetails>")!;
var fields = actualPrivateImplementationDetails.GetFields(BindingFlags.NonPublic | BindingFlags.Static);
return [
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Int32_12, fields),
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Int32_16, fields),
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Int64_24, fields),
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Byte_6, fields),
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Boolean_5, fields),
TestCaseDataFor(() => ArraysForOptimizedFieldNameTests.Char_10, fields)
];

static TestCaseData TestCaseDataFor<T>(Expression<Func<T[]>> expression, FieldInfo[] fields)
{
Boolean b;
var fieldExpression = (MemberExpression) expression.Body;
var fieldName = fieldExpression.Member.Name.AsSpan();
var sizeSeparatorIndex = fieldName.IndexOf('_');
var sizeSpan = fieldName.Slice(sizeSeparatorIndex + 1);
var fieldTypeName = $"__StaticArrayInitTypeSize={Int32.Parse(sizeSpan)}";

var x = expression.Compile();
var arrayValues = x();

return new TestCaseData(
$"{arrayValues[0].GetType().FullName}[] _array = [ {string.Join(',', arrayValues.Select(item => item.ToString()!.ToLower()))}]", // Array
// Marshal.SizeOf() returns the unmanaged size of the type which does not match for System.Char / System.Boolean
arrayValues[0].GetType().FullName switch
{
"System.Char" => sizeof(char),
"System.Boolean" => sizeof(bool),
_ => Marshal.SizeOf(arrayValues[0].GetType())
},
fields.Single(f => f.FieldType.Name == fieldTypeName).Name // expected field name
).SetName($"{arrayValues.Length} {arrayValues[0].GetType().Name}");
}
}

static class ArraysForOptimizedFieldNameTests
{
public static int[] Int32_12 = [2, 4, 6]; // 3 * sizeof(int)
public static int[] Int32_16 = [2, 4, 6, 8]; // 4 * sizeof(int)
public static long[] Int64_24 = [42, 314, 5]; // 3 * sizeof(long)
public static byte[] Byte_6 = [1, 2, 3, 4, 5, 6]; // 6 * sizeof(byte)
public static bool[] Boolean_5 = [true, true, true, false, false]; // 5 * sizeof(bool); We need at least 5 bools for the optimization to kick in and add the extra type.
public static char[] Char_10 = ['1','2','3','4','5']; // 5 * sizeof(char)
}
}
2 changes: 1 addition & 1 deletion Cecilifier.Core.Tests/Tests/Unit/StackallocTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public void InitializationOptimized(string storageType)
\s+\1.NestedTypes.Add\(\2\);
\s+var (fld_arrayInitializerData_\d+) = new FieldDefinition\("[A-Z0-9]+", FieldAttributes.Assembly \| FieldAttributes.Static \| FieldAttributes.InitOnly, \2\);
\s+\1.Fields.Add\(\3\);
\s+\3.InitialValue = Cecilifier.Runtime.TypeHelpers.ToByteArray<Byte>\(stackalloc Byte\[\] { 1, 2, 3 }\);
\s+\3.InitialValue = \[ 0x01,0x02,0x03, \];
\s+//duplicates the top of the stack \(the newly `stackalloced` buffer\) and initialize it from the raw buffer \(\3\).
\s+(il_.+).Emit\(OpCodes.Dup\);
\s+\4.Emit\(OpCodes.Ldsflda, \3\);
Expand Down
8 changes: 5 additions & 3 deletions Cecilifier.Core/AST/ArrayInitializationProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using Cecilifier.Core.CodeGeneration;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Misc;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;

Expand Down Expand Up @@ -60,11 +61,12 @@ internal static void InitializeOptimized<TElement>(ExpressionVisitor visitor, IT
//IL_0007: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=24' '<PrivateImplementationDetails>'::'5BC33F8E8CDE3A32E1CF1EE1B1771AC0400514A8675FC99966FCAE1E8184FDFE'
//IL_000c: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [System.Runtime]System.Array, valuetype [System.Runtime]System.RuntimeFieldHandle)
//IL_0011: pop

var backingFieldVar = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(
context,
elementType.SizeofArrayLikeItemElement() * elements.Count,
elementType.Name,
$"new {elementType.Name}[] {{ {elements.ToFullString()}}}");
elementType.SizeofPrimitiveType(),
elements.Select(item => item.DescendantNodesAndSelf().OfType<LiteralExpressionSyntax>().Single().Token.ValueText).ToArray(),
StringToSpanOfBytesConverters.For(elementType.FullyQualifiedName()));

context.EmitCilInstruction(visitor.ILVariable, OpCodes.Dup);
context.EmitCilInstruction(visitor.ILVariable, OpCodes.Ldtoken, backingFieldVar);
Expand Down
16 changes: 10 additions & 6 deletions Cecilifier.Core/AST/ExpressionVisitor.Stackalloc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public override void VisitStackAllocArrayCreationExpression(StackAllocArrayCreat
var stackallocSpanAssignmentTracker = new StackallocSpanAssignmentTracker(node, Context);

var arrayElementTypeSize = arrayElementType.Type.IsPrimitiveType()
? arrayElementType.Type.SizeofArrayLikeItemElement()
: uint.MaxValue; // this means the size of the elements need to be calculated at runtime...
? arrayElementType.Type.SizeofPrimitiveType()
: int.MaxValue; // this means the size of the elements need to be calculated at runtime...

var resolvedElementType = ResolveType(arrayType.ElementType);
if (rankNode.IsKind(SyntaxKind.NumericLiteralExpression) && arrayElementType.Type.IsPrimitiveType())
Expand Down Expand Up @@ -120,8 +120,12 @@ void ProcessStackAllocInitializer(InitializerExpressionSyntax node)
private void EmitOptimizedInitialization(InitializerExpressionSyntax node, TypeInfo typeInfo)
{
var arrayType = (typeInfo.Type ?? typeInfo.ConvertedType).ElementTypeSymbolOf();
var sizeInBytes = arrayType.SizeofArrayLikeItemElement() * node.Expressions.Count;
var fieldWithRawData = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(Context, sizeInBytes, arrayType.Name, $"stackalloc {arrayType.Name}[] {node.ToFullString()}");
var sizeInBytes = arrayType.SizeofPrimitiveType() * node.Expressions.Count;
var fieldWithRawData = PrivateImplementationDetailsGenerator.GetOrCreateInitializationBackingFieldVariableName(
Context,
arrayType.SizeofPrimitiveType(),
node.Expressions.Select(item => item.DescendantNodesAndSelf().OfType<LiteralExpressionSyntax>().Single().Token.ValueText).ToArray(),
StringToSpanOfBytesConverters.For(arrayType.FullyQualifiedName()));

Context.WriteNewLine();
Context.WriteComment($"duplicates the top of the stack (the newly `stackalloced` buffer) and initialize it from the raw buffer ({fieldWithRawData}).");
Expand All @@ -136,8 +140,8 @@ private void EmitOptimizedInitialization(InitializerExpressionSyntax node, TypeI
private void EmitSlowInitialization(InitializerExpressionSyntax node, TypeInfo typeInfo)
{
var arrayType = typeInfo.Type ?? typeInfo.ConvertedType;
uint elementTypeSize = arrayType.SizeofArrayLikeItemElement();
uint offset = 0;
int elementTypeSize = arrayType.SizeofPrimitiveType();
int offset = 0;

foreach (var exp in node.Expressions)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using Cecilifier.Core.AST;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Misc;
Expand Down Expand Up @@ -259,11 +264,18 @@ private static string ResolveOwnedGenericParameter(IVisitorContext context, stri

return spanTypeParameter.VariableName;
}
internal static string GetOrCreateInitializationBackingFieldVariableName(IVisitorContext context, long sizeInBytes, string arrayElementTypeName, string initializationExpression)

internal static string GetOrCreateInitializationBackingFieldVariableName(IVisitorContext context, int elementSizeInBytes, IList<string> elements, SpanAction<byte, string> converter)
{
Span<byte> toBeHashed = stackalloc byte[System.Text.Encoding.UTF8.GetByteCount(initializationExpression)];
System.Text.Encoding.UTF8.GetBytes(initializationExpression, toBeHashed);
var bufferSize = elementSizeInBytes * elements.Count;
byte[] toReturn = null;
var toBeHashed = bufferSize <= Constants.MaxStackAlloc ? stackalloc byte[bufferSize] : toReturn = ArrayPool<byte>.Shared.Rent(bufferSize);
Span<byte> target = toBeHashed;
foreach (var element in elements)
{
converter(target, element);
target = target.Slice(elementSizeInBytes);
}

Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(toBeHashed, hash);
Expand All @@ -274,7 +286,7 @@ internal static string GetOrCreateInitializationBackingFieldVariableName(IVisito
return found.VariableName;

var privateImplementationDetailsVar = GetOrCreatePrivateImplementationDetailsTypeVariable(context);
var rawDataTypeVar = GetOrCreateRawDataType(context, sizeInBytes);
var rawDataTypeVar = GetOrCreateRawDataType(context, bufferSize);

// Add a field to hold the static initialization data.
//
Expand All @@ -292,12 +304,23 @@ internal static string GetOrCreateInitializationBackingFieldVariableName(IVisito
rawDataTypeVar,
Constants.CompilerGeneratedTypes.StaticArrayInitFieldModifiers);
context.WriteCecilExpressions(fieldExpressions);
context.WriteCecilExpression($"{fieldVar}.InitialValue = Cecilifier.Runtime.TypeHelpers.ToByteArray<{arrayElementTypeName}>({initializationExpression});");
var initializationByteArrayAsString = new StringBuilder();
foreach (var itemValue in toBeHashed)
{
initializationByteArrayAsString.Append($"0x{itemValue:x2},");
}

context.WriteCecilExpression($"{fieldVar}.InitialValue = [ { initializationByteArrayAsString } ];");
context.WriteNewLine();

if (toReturn is not null)
{
ArrayPool<byte>.Shared.Return(toReturn);
}

return context.DefinitionVariables.GetVariable(fieldName, VariableMemberKind.Field, Constants.CompilerGeneratedTypes.PrivateImplementationDetails);
}

private static string GetOrCreateRawDataType(IVisitorContext context, long sizeInBytes)
{
if (sizeInBytes == sizeof(int))
Expand Down
Loading

0 comments on commit 749380e

Please sign in to comment.