Skip to content

Commit

Permalink
adds basic support for inline arrays (#257)
Browse files Browse the repository at this point in the history
there are at least 2 cases (marked as TODOs) not supported
  • Loading branch information
adrianoc committed Mar 19, 2024
1 parent 9e08fce commit 453317a
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.locals init (ByteBuffer V_0, InterfaceBuffer V_1, StructBuffer V_2, SomeStruct V_3)
IL_0000: ldloca.s V_0
IL_0002: initobj ByteBuffer
IL_0008: ldloca V_0
IL_000c: call TElement& <PrivateImplementationDetails>::InlineArrayFirstElementRef<ByteBuffer,System.Byte>(TBuffer&)
IL_0011: ldc.i4 1
IL_0016: stind.i1
IL_0017: ldloca V_0
IL_001b: ldc.i4 1
IL_0020: call TElement& <PrivateImplementationDetails>::InlineArrayElementRef<ByteBuffer,System.Byte>(TBuffer&,System.Int32)
IL_0025: ldloca V_0
IL_0029: call TElement& <PrivateImplementationDetails>::InlineArrayFirstElementRef<ByteBuffer,System.Byte>(TBuffer&)
IL_002e: ldind.u1
IL_002f: ldc.i4 1
IL_0034: add
IL_0035: conv.u1
IL_0036: stind.i1
IL_0037: ldloca V_0
IL_003b: ldc.i4 1
IL_0040: call TElement& <PrivateImplementationDetails>::InlineArrayElementRef<ByteBuffer,System.Byte>(TBuffer&,System.Int32)
IL_0045: ldind.u1
IL_0046: call System.Void System.Console::WriteLine(System.Int32)
IL_004b: ldloca.s V_1
IL_004d: initobj InterfaceBuffer
IL_0053: ldloca V_1
IL_0057: call TElement& <PrivateImplementationDetails>::InlineArrayFirstElementRef<InterfaceBuffer,System.IComparable>(TBuffer&)
IL_005c: ldc.i4 1
IL_0061: box System.Int32
IL_0066: stind.ref
IL_0067: ldloca.s V_2
IL_0069: initobj StructBuffer
IL_006f: ldloca V_2
IL_0073: call TElement& <PrivateImplementationDetails>::InlineArrayFirstElementRef<StructBuffer,SomeStruct>(TBuffer&)
IL_0078: ldloca.s V_3
IL_007a: initobj SomeStruct
IL_0080: ldloca.s V_3
IL_0082: dup
IL_0083: ldc.i4 42
IL_0088: stfld System.Int32 SomeStruct::Value
IL_008d: pop
IL_008e: ldloc V_3
IL_0092: stobj SomeStruct
IL_0097: ldloca V_2
IL_009b: call TElement& <PrivateImplementationDetails>::InlineArrayFirstElementRef<StructBuffer,SomeStruct>(TBuffer&)
IL_00a0: ldfld System.Int32 SomeStruct::Value
IL_00a5: call System.Void System.Console::WriteLine(System.Int32)
IL_00aa: ret
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Runtime.CompilerServices;

class InlineArrayTests
{
void Test()
{
var byteBuffer = new ByteBuffer();
byteBuffer[0] = 1;
byteBuffer[1] = (byte) (byteBuffer[0] + 1);
Console.WriteLine(byteBuffer[1]);

var interfaceBuffer = new InterfaceBuffer();
interfaceBuffer[0] = 1;

var StructBuffer = new StructBuffer();
StructBuffer[0] = new SomeStruct { Value = 42 };
Console.WriteLine(StructBuffer[0].Value);
}
}
[InlineArray(2)]
struct ByteBuffer
{
private byte _data;
}

[InlineArray(2)]
struct InterfaceBuffer
{
private IComparable _data;
}

struct SomeStruct
{
public int Value;
}

[InlineArray(2)]
struct StructBuffer
{
private SomeStruct _data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Cecilifier.Core.Tests.Framework;
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Integration;

public class InlineArraysTests : ResourceTestBase
{
[Test]
public void TestInlineArrays()
{
AssertResourceTestWithExplicitExpectation("Misc/InlineArrays", "System.Void InlineArrayTests::Test()");
}
}
160 changes: 159 additions & 1 deletion Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,164 @@ public struct IntBuffer { private int _element0; }
"""));
}

[TestCase("string", "\"Foo\"", """
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldstr, "Foo"\);
\1Stind_Ref\);
""", TestName = "String constant")]

// Element type: reference type, value type, custom value type
[TestCase("object", "null", """
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldnull\);
\1Stind_Ref\);
""", TestName = "Object (null)")]

[TestCase("CustomStruct", "new CustomStruct(42)", """
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldc_I4, 42\);
\1Newobj, ctor_customStruct_\d+\);
\1Stobj\);
""", TestName = "Custom struct (new expression)")]

[TestCase("CustomStruct", "new CustomStruct { name = \"Foo\" }", """
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\s+var (l_vt_\d+) = new VariableDefinition\((?<struct_type>st_customStruct_\d+)\);
\s+m_M_\d+\.Body\.Variables.Add\(\2\);
(?<lsa>\1Ldloca_S, \2\);)
\1Initobj, \k<struct_type>\);
\k<lsa>
\1Dup\);
\1Ldstr, "Foo"\);
\1Stfld, fld_name_\d+\);
\1Pop\);
\1Ldloc, \2\);
\1Stobj, \k<struct_type>\);
""", TestName = "Custom struct (object initializer)")]
public void InlineArray_ElementType(string elementType, string value, string expectedIL)
{
var result = RunCecilifier($$"""
class C
{
void M(Buffer b, {{elementType}} value)
{
b[0] = {{value}};
b[1] = value;
}
}
struct CustomStruct
{
public CustomStruct(int value) {}
public string name;
}
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer { private {{elementType}} _element0; }
""");

var cecilified = result.GeneratedCode.ReadToEnd();

Assert.That(cecilified, Does.Match(expectedIL));
}

[Test]
public void InlineArray_MemberAccess_OnIndex()
{
var result = RunCecilifier($$"""
class C
{
int M(Buffer b) => b[0].Value;
}
struct CustomStruct
{
public int Value;
}
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer { private CustomStruct _element0; }
""");

var cecilified = result.GeneratedCode.ReadToEnd();

Assert.That(cecilified, Does.Match("""
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldfld, fld_value_\d+\);
"""));
}

[TestCase("T M<T>(Buffer<T> b) => b[0];",
"""
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldobj, gp_T_14\);
""", TestName = "Open generic method")]

[TestCase("int M(Buffer<int> bi) => bi[0];",
"""
\s+il_M_14.Emit\(OpCodes.Call, gi_inlineArrayFirstElementRef_\d+\);
\s+il_M_14.Emit\(OpCodes.Ldind_I4\);
""", TestName = "Closed generic type (primitive type)")]

[TestCase("CustomStruct M(Buffer<CustomStruct> b) => b[0];",
"""
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldobj, st_customStruct_0\);
""", TestName = "Closed generic type (custom struct type)")]

[TestCase("TC M(Buffer<TC> b) => b[0];",
"""
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, gi_inlineArrayFirstElementRef_\d+\);
\1Ldobj, gp_tC_\d+\);
""", TestName = "Type Parameter from declaring type")]

[TestCase("T M<T>(Buffer<T> b) where T : Itf => b[0];",
"""
(gi_inlineArrayFirstElementRef_\d+).GenericArguments.Add\((gp_T_\d+)\);
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, \1\);
\3Ldobj, \2\);
""", TestName = "Interface as Type Parameter")]

//TODO: accessing a property/method/event from an interface on a generic type must be constrained
// The code is handling 'non inline array' (see example https://cutt.ly/constrained_non_inlinearray)
[TestCase("int M<T>(Buffer<T> b) where T : Itf => b[0].Value;",
"""
(gi_inlineArrayFirstElementRef_\d+).GenericArguments.Add\((gp_T_\d+)\);
(\s+il_M_\d+\.Emit\(OpCodes\.)Call, \1\);
\3Constrained, \2\);
\3Callvirt, m_get_\d+\);
""", IgnoreReason = "Accessing a property/method/event from an interface on a generic type must be constrained", TestName = "Member access on interface")]
public void InlineArray_ElementAccess_OnGenericType(string toBeTested, string expecetdIL)
{
var result = RunCecilifier($$"""
struct CustomStruct { public int Value; }
public interface Itf { int Value { get; set; } }
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer<T> { private T _element0; }
class C<TC> { {{toBeTested}} }
""");

var cecilified = result.GeneratedCode.ReadToEnd();
Assert.That(cecilified, Does.Match(expecetdIL));
}

//TODO: InlineArray : Implement support for slicing through ranges
[Test]
public void Range()
{
var result = RunCecilifier("""
class C
{
System.Span<int> M(ref Buffer b) => b[1..3];
}
[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer { private int _element0; }
""");

var cecilified = result.GeneratedCode.ReadToEnd();
Assert.Ignore($"Just for testing. Output:\n{cecilified}");
}
}
14 changes: 13 additions & 1 deletion Cecilifier.Core/AST/ExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,19 @@ public override void VisitElementAccessExpression(ElementAccessExpressionSyntax

if (InlineArrayProcessor.TryHandleInlineArrayElementAccess(Context, ilVar, node, out var elementType))
{
Context.EmitCilInstruction(ilVar, elementType.LdindOpCodeFor());
// if the parent of the element access expression is a member access expression the code
// that handles that expects that the target instance is at the top of the stack so; in
// the case of that target being an inline array element, that means that the address of
// the entry should be at the top of the stack which is exactly how
// TryHandleInlineArrayElementAccess() will leave the stack so in this case there's nothing.
// else to be done.
// Otherwise, we need to take the top of the stack (address of the element) and load the
// actual instance to the stack.
if (!node.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
var loadOpCode = elementType.LdindOpCodeFor();
Context.EmitCilInstruction(ilVar, loadOpCode, loadOpCode == OpCodes.Ldobj ? Context.TypeResolver.Resolve(elementType) : null);
}
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ internal NoArgsValueTypeObjectCreatingInAssignmentVisitor(IVisitorContext ctx, s

public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node)
{
if (InlineArrayProcessor.TryHandleInlineArrayElementAccess(Context, ilVar, node, out var elementType))
{
var tempVar = tempValueTypeDeclarer();
Context.EmitCilInstruction(ilVar, OpCodes.Ldloc, tempVar.VariableName);
Context.EmitCilInstruction(ilVar, OpCodes.Stobj, Context.TypeResolver.Resolve(elementType));
return;
}

ExpressionVisitor.Visit(Context, ilVar, node);

//ExpressionVisitor assumes the visited expression is to be handled as a 'load'
Expand Down
2 changes: 1 addition & 1 deletion Cecilifier.Core/AST/ValueTypeNoArgCtorInvocationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Cecilifier.Core.AST
// the case when the result of the 'new T()' expression is not used as a) a direct assignment to a value type variable
// or b) as the initializer in the declaration of the value type variable.
//
// This visitor expects visit the parent of the ObjectCreationExpression, for instance, in 'M(new T())', this visitor
// This visitor expects to visit the parent of the ObjectCreationExpression, for instance, in 'M(new T())', this visitor
// should be called to visit the method invocation.
//
// Note that the generated code will hardly match the generated code by a C# compiler. Empirically we observed that
Expand Down
2 changes: 1 addition & 1 deletion Cecilifier.Core/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public static OpCode LdindOpCodeFor(this ITypeSymbol type)
SpecialType.System_Boolean => OpCodes.Ldind_U1,
SpecialType.System_Object => OpCodes.Ldind_Ref,

_ => type.IsValueType ? OpCodes.Ldobj : OpCodes.Ldind_Ref
_ => type.IsValueType || type.IsTypeParameterOrIsGenericTypeReferencingTypeParameter() ? OpCodes.Ldobj : OpCodes.Ldind_Ref
};
}

Expand Down

0 comments on commit 453317a

Please sign in to comment.