Skip to content

Commit

Permalink
adds inline arrays support (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianoc committed Mar 27, 2024
1 parent 0eb8842 commit 33a2f5c
Show file tree
Hide file tree
Showing 11 changed files with 598 additions and 23 deletions.
89 changes: 89 additions & 0 deletions Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Tests.Unit;

[TestFixture]
public class InlineArrayTests : CecilifierUnitTestBase
{
[Test]
public void Instantiating_InlineArray_EmitsInitObj()
{
var result = RunCecilifier("""
var b = new IntBuffer();
[System.Runtime.CompilerServices.InlineArray(10)]
public struct IntBuffer
{
private int _element0;
}
""");

Assert.That(result.GeneratedCode.ReadToEnd(), Does.Match(
@"m_topLevelStatements_\d+.Body.Variables.Add\((?<ia_var>l_b_\d+)\);\s+"+ // local variable *b*
@"(?<emit>il_topLevelMain_\d+.Emit\(OpCodes\.)Ldloca_S, \k<ia_var>\);\s+" + // Loads *b* address
@"\k<emit>Initobj, st_intBuffer_\d+\);")); // Execute *initobj* on *b*
}

[TestCase("System.Span<int> span = l;", TestName = "Local variable initialization")]
[TestCase("scoped System.Span<int> span; span = l;", TestName = "Local Variable assignment")]
[TestCase("Consume(l);", TestName = "Local passed as argument")]
[TestCase("Consume(p);", TestName = "Parameter passed as argument")]
public void Assigning_InlineArrayToSpan_EmitsPrivateImplementationDetailsType(string triggeringStatements)
{
var result = RunCecilifier($$"""
void TestMethod(IntBuffer p)
{
var l = new IntBuffer();
// This will trigger the emission of <PrivateImplementationDetails>.InlineArrayAsSpan() method
{{triggeringStatements}}
}
void Consume(System.Span<int> span) {}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct IntBuffer
{
private int _element0;
}
""");

var cecilifiedCode = result.GeneratedCode.ReadToEnd();
Assert.That(cecilifiedCode, Does.Match("""new TypeDefinition\("", "<PrivateImplementationDetails>", .+\)"""));
}

[Test]
public void AccessToFirstElement_MapsTo_PrivateImplementationDetailsInlineArrayFirstElementRefMethod()
{
var result = RunCecilifier("""
var buffer = new IntBuffer();
buffer[0] = 42;
[System.Runtime.CompilerServices.InlineArray(10)]
public struct IntBuffer
{
private int _element0;
}
""");

var cecilified = result.GeneratedCode.ReadToEnd();

// assert that the inline array address is being pushed to the stack...
Assert.That(cecilified, Does.Match("""
il_topLevelMain_\d+\.Emit\(OpCodes\.Ldloca, l_buffer_\d+\);
\s+//<PrivateImplementationDetails> class.
"""));

// and later <PrivateImplementationDetails>.InlineArrayFirstElementRef() static method is being invoked
// and the value 42 stored in the address at the top of the stack.
Assert.That(cecilified, Does.Match("""
(il_topLevelMain_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\s+\1Ldc_I4, 42\);
\s+\1Stind_I4
"""));
}

// Access to not first element
//
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class PrivateImplementationDetailsGeneratorTests
[Test]
public void PrivateImplementationType_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.Type);
Expand All @@ -33,7 +33,7 @@ public void PrivateImplementationType_IsCached()
[Test]
public void Int32AndInt64_AreUsedAsFieldBackingType_OfArraysOf4And8Bytes()
{
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.Type);
Expand Down Expand Up @@ -93,6 +93,26 @@ public void BackingField_IsUniquePerDataSize()
Assert.That(found.Count(), Is.EqualTo(2), context.Output);
Assert.That(secondVariableName, Is.Not.EqualTo(variableName), context.Output);
}

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

var found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Method).ToArray();
Assert.That(found.Length, Is.EqualTo(0));

// internal static Span<TElement> InlineArrayAsSpan<TBuffer, TElement>(ref TBuffer buffer, int length)
var methodVariableName = PrivateImplementationDetailsGenerator.GetOrEmmitInlineArrayAsSpanMethod(context);
found = context.DefinitionVariables.GetVariablesOf(VariableMemberKind.Method).ToArray();
Assert.That(found.Length, Is.EqualTo(1));
Assert.That(found[0].MemberName, Is.EqualTo("InlineArrayAsSpan"));

Assert.That(context.Output, Does.Match("""var m_inlineArrayAsSpan_\d+ = new MethodDefinition\("InlineArrayAsSpan", MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, assembly.MainModule.TypeSystem.Void\);"""));
Assert.That(context.Output, Does.Match("""m_inlineArrayAsSpan_\d+.Parameters.Add\(new ParameterDefinition\("buffer", ParameterAttributes.None, gp_tBuffer_\d+.MakeByReferenceType\(\)\)\);"""));
Assert.That(context.Output, Does.Match("""m_inlineArrayAsSpan_\d+.Parameters.Add\(new ParameterDefinition\("length", ParameterAttributes.None, assembly.MainModule.TypeSystem.Int32\)\);"""));
}

static CSharpCompilation CompilationFor(string code)
{
Expand Down
13 changes: 10 additions & 3 deletions Cecilifier.Core/AST/AssignmentVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Misc;
using Cecilifier.Core.Variables;
Expand Down Expand Up @@ -44,13 +45,20 @@ internal AssignmentVisitor(IVisitorContext ctx, string ilVar) : base(ctx)
public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node)
{
var lastInstructionLoadingRhs = Context.CurrentLine;

if (InlineArrayProcessor.HandleInlineArrayElementAccess(Context, ilVar, node))
{
Context.MoveLinesToEnd(InstructionPrecedingValueToLoad, lastInstructionLoadingRhs);
var arrayElementType = Context.SemanticModel.GetTypeInfo(node).Type.EnsureNotNull();
Context.EmitCilInstruction(ilVar, arrayElementType.Stind());
return;
}

ExpressionVisitor.Visit(Context, ilVar, node.Expression);
foreach (var arg in node.ArgumentList.Arguments)
{
ExpressionVisitor.Visit(Context, ilVar, arg);
}

if (!HandleIndexer(node, lastInstructionLoadingRhs))
{
Context.MoveLinesToEnd(InstructionPrecedingValueToLoad, lastInstructionLoadingRhs);
Expand Down Expand Up @@ -207,7 +215,6 @@ bool HandleIndexer(SyntaxNode node, LinkedListNode<string> lastInstructionLoadin

return true;
}

private void EmitIndirectStore(ITypeSymbol typeBeingStored)
{
var indirectStoreOpCode = typeBeingStored.Stind();
Expand Down
153 changes: 153 additions & 0 deletions Cecilifier.Core/AST/InlineArrayProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Cecilifier.Core.CodeGeneration;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Naming;
using Cecilifier.Core.Variables;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Mono.Cecil.Cil;

namespace Cecilifier.Core.AST;
public class InlineArrayProcessor
{
internal static bool HandleInlineArrayConversionToSpan(IVisitorContext context, string ilVar, ITypeSymbol fromType, SyntaxNode fromNode, OpCode opcode, string name, VariableMemberKind memberKind, string parentName = null)
{
int inlineArrayLength = InlineArrayLengthFrom(fromType);
if (inlineArrayLength == -1)
return false;

if (!IsNodeUsedToInitializeSpanLocalVariable(context, fromNode)
&& !IsNodeAssignedToLocalVariable(context, fromNode)
&& !IsNodeUsedAsSpanArgument(context, fromNode))
return false;

// ldloca.s address of fromNode.
// ldci4 fromNode.Length (size of the inline array)
context.EmitCilInstruction(ilVar, opcode, context.DefinitionVariables.GetVariable(name, memberKind, parentName).VariableName);
context.EmitCilInstruction(ilVar, OpCodes.Ldc_I4, inlineArrayLength);
context.EmitCilInstruction(ilVar, OpCodes.Call, InlineArrayAsSpanMethodFor(context, fromType));
return true;

static bool IsNodeAssignedToLocalVariable(IVisitorContext context, SyntaxNode nodeToCheck)
{
if (nodeToCheck.Parent is not AssignmentExpressionSyntax assignmentExpression)
return false;

var lhs = context.SemanticModel.GetSymbolInfo(assignmentExpression.Left);
if (lhs.Symbol == null)
return false;

return SymbolEqualityComparer.Default.Equals(lhs.Symbol.GetMemberType().OriginalDefinition, context.RoslynTypeSystem.SystemSpan);
}

static bool IsNodeUsedAsSpanArgument(IVisitorContext context, SyntaxNode nodeToCheck)
{
if (nodeToCheck.Parent is not ArgumentSyntax argumentSyntax)
return false;

var argumentIndex = ((ArgumentListSyntax) argumentSyntax.Parent).Arguments.IndexOf(argumentSyntax);
var invocation = argumentSyntax.FirstAncestorOrSelf<InvocationExpressionSyntax>();
var associatedParameterSymbol = ((IMethodSymbol) context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol).Parameters.ElementAtOrDefault(argumentIndex);

return SymbolEqualityComparer.Default.Equals(associatedParameterSymbol.Type.OriginalDefinition, context.RoslynTypeSystem.SystemSpan);
}

static bool IsNodeUsedToInitializeSpanLocalVariable(IVisitorContext context, SyntaxNode nodeToCheck)
{
var parent = nodeToCheck.Parent;
if (!parent.IsKind(SyntaxKind.EqualsValueClause) || !parent.Parent.IsKind(SyntaxKind.VariableDeclarator))
return false;

var variableDeclaration = (VariableDeclarationSyntax) parent.Parent.Parent!;
var declaredVariableType = ModelExtensions.GetTypeInfo(context.SemanticModel, variableDeclaration.Type);

return SymbolEqualityComparer.Default.Equals(declaredVariableType.Type?.OriginalDefinition, context.RoslynTypeSystem.SystemSpan);
}

static string InlineArrayAsSpanMethodFor(IVisitorContext context, ITypeSymbol inlineArrayType)
{
return PrivateImplementationInlineArrayGenericInstanceMethodFor(
context,
PrivateImplementationDetailsGenerator.GetOrEmmitInlineArrayAsSpanMethod(context),
"InlineArrayAsSpan",
inlineArrayType);
}
}

internal static bool HandleInlineArrayElementAccess(IVisitorContext context, string ilVar, ElementAccessExpressionSyntax elementAccess)
{
if (elementAccess.Expression.IsKind(SyntaxKind.ElementAccessExpression))
return false;

var expSymbol = context.SemanticModel.GetSymbolInfo(elementAccess.Expression).Symbol.EnsureNotNull();
if (expSymbol.GetMemberType().TryGetAttribute<InlineArrayAttribute>(out _))
{
ExpressionVisitor.Visit(context, ilVar, elementAccess.Expression);
Debug.Assert(elementAccess.ArgumentList.Arguments.Count == 1);

var method = string.Empty;
if (elementAccess.ArgumentList.Arguments[0].Expression.TryGetLiteralValueFor(out int index) && index == 0)
{
method = InlineArrayFirstElementRefMethodFor(context, expSymbol.GetMemberType());
}
else
{
context.EmitCilInstruction(ilVar, OpCodes.Ldc_I4, index);
method = InlineArrayElementRefMethodFor(context, expSymbol.GetMemberType());
}
context.EmitCilInstruction(ilVar, OpCodes.Call, method);

return true;
}

return false;

static string InlineArrayFirstElementRefMethodFor(IVisitorContext context, ITypeSymbol inlineArrayType)
{
return PrivateImplementationInlineArrayGenericInstanceMethodFor(
context,
PrivateImplementationDetailsGenerator.GetOrEmmitInlineArrayFirstElementRefMethod(context),
"InlineArrayFirstElementRef",
inlineArrayType);
}

static string InlineArrayElementRefMethodFor(IVisitorContext context, ITypeSymbol inlineArrayType)
{
return PrivateImplementationInlineArrayGenericInstanceMethodFor(
context,
PrivateImplementationDetailsGenerator.GetOrEmmitInlineArrayElementRefMethod(context),
"InlineArrayElementRef",
inlineArrayType);
}
}

private static string PrivateImplementationInlineArrayGenericInstanceMethodFor(IVisitorContext context, string openGenericTypeVar, string methodName, ITypeSymbol inlineArrayType)
{
var varName = context.Naming.SyntheticVariable(methodName, ElementKind.GenericInstance);

var exps = openGenericTypeVar.MakeGenericInstanceMethod(
varName,
[
context.TypeResolver.Resolve(inlineArrayType), // TBuffer
context.TypeResolver.Resolve(((IFieldSymbol) inlineArrayType.GetMembers().First()).Type) // TElement
]);

foreach (var exp in exps)
{
context.WriteCecilExpression(exp);
context.WriteNewLine();
}

return varName;
}

private static int InlineArrayLengthFrom(ITypeSymbol rhsType)
{
return rhsType.TryGetAttribute<InlineArrayAttribute>(out var inlineArrayAttribute)
? (int) inlineArrayAttribute.ConstructorArguments.First().Value
: -1;
}
}
Loading

0 comments on commit 33a2f5c

Please sign in to comment.