diff --git a/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.il.txt b/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.il.txt new file mode 100644 index 00000000..ea597b84 --- /dev/null +++ b/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.il.txt @@ -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& ::InlineArrayFirstElementRef(TBuffer&) +IL_0011: ldc.i4 1 +IL_0016: stind.i1 +IL_0017: ldloca V_0 +IL_001b: ldc.i4 1 +IL_0020: call TElement& ::InlineArrayElementRef(TBuffer&,System.Int32) +IL_0025: ldloca V_0 +IL_0029: call TElement& ::InlineArrayFirstElementRef(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& ::InlineArrayElementRef(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& ::InlineArrayFirstElementRef(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& ::InlineArrayFirstElementRef(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& ::InlineArrayFirstElementRef(TBuffer&) +IL_00a0: ldfld System.Int32 SomeStruct::Value +IL_00a5: call System.Void System.Console::WriteLine(System.Int32) +IL_00aa: ret diff --git a/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.txt b/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.txt new file mode 100644 index 00000000..404d78d5 --- /dev/null +++ b/Cecilifier.Core.Tests/TestResources/Integration/Misc/InlineArrays.cs.txt @@ -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; +} diff --git a/Cecilifier.Core.Tests/Tests/Integration/Miscelaneous/InlineArraysTests.cs b/Cecilifier.Core.Tests/Tests/Integration/Miscelaneous/InlineArraysTests.cs new file mode 100644 index 00000000..4b5bb634 --- /dev/null +++ b/Cecilifier.Core.Tests/Tests/Integration/Miscelaneous/InlineArraysTests.cs @@ -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()"); + } +} diff --git a/Cecilifier.Core.Tests/Tests/Integration/MiscTestCase.cs b/Cecilifier.Core.Tests/Tests/Integration/Miscelaneous/MiscTestCase.cs similarity index 100% rename from Cecilifier.Core.Tests/Tests/Integration/MiscTestCase.cs rename to Cecilifier.Core.Tests/Tests/Integration/Miscelaneous/MiscTestCase.cs diff --git a/Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs b/Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs index 981923de..f1b9f06c 100644 --- a/Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs +++ b/Cecilifier.Core.Tests/Tests/Unit/InlineArrayTests.cs @@ -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\((?st_customStruct_\d+)\); + \s+m_M_\d+\.Body\.Variables.Add\(\2\); + (?\1Ldloca_S, \2\);) + \1Initobj, \k\); + \k + \1Dup\); + \1Ldstr, "Foo"\); + \1Stfld, fld_name_\d+\); + \1Pop\); + \1Ldloc, \2\); + \1Stobj, \k\); + """, 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(Buffer 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 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 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 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(Buffer 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(Buffer 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 { private T _element0; } + + class C { {{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 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}"); + } } diff --git a/Cecilifier.Core/AST/ExpressionVisitor.cs b/Cecilifier.Core/AST/ExpressionVisitor.cs index 243d9ae1..37243d97 100644 --- a/Cecilifier.Core/AST/ExpressionVisitor.cs +++ b/Cecilifier.Core/AST/ExpressionVisitor.cs @@ -243,7 +243,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; } diff --git a/Cecilifier.Core/AST/NoArgsValueTypeObjectCreatingInAssignmentVisitor.cs b/Cecilifier.Core/AST/NoArgsValueTypeObjectCreatingInAssignmentVisitor.cs index e965c170..7562e0d8 100644 --- a/Cecilifier.Core/AST/NoArgsValueTypeObjectCreatingInAssignmentVisitor.cs +++ b/Cecilifier.Core/AST/NoArgsValueTypeObjectCreatingInAssignmentVisitor.cs @@ -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' diff --git a/Cecilifier.Core/AST/ValueTypeNoArgCtorInvocationVisitor.cs b/Cecilifier.Core/AST/ValueTypeNoArgCtorInvocationVisitor.cs index 12cfec02..9f293020 100644 --- a/Cecilifier.Core/AST/ValueTypeNoArgCtorInvocationVisitor.cs +++ b/Cecilifier.Core/AST/ValueTypeNoArgCtorInvocationVisitor.cs @@ -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 diff --git a/Cecilifier.Core/Extensions/TypeExtensions.cs b/Cecilifier.Core/Extensions/TypeExtensions.cs index 2c4026c9..400175f2 100644 --- a/Cecilifier.Core/Extensions/TypeExtensions.cs +++ b/Cecilifier.Core/Extensions/TypeExtensions.cs @@ -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 }; }