diff --git a/Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs b/Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs index e1495dfd..aea7e57a 100644 --- a/Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs +++ b/Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs @@ -125,7 +125,33 @@ public void OpenIEnumerable() """)); Assert.That(cecilifiedCode, Does.Match("""var l_openget_Current_\d+ = .+ImportReference\(typeof\(.+IEnumerator<>\)\).Resolve\(\).Methods.First\(m => m.Name == "get_Current"\);""")); } - + + [Test] + public void Array() + { + var result = RunCecilifier(""" + foreach(var v in new int[] {1, 2, 3, 4}) + System.Console.WriteLine(v); + """); + + var cecilifiedCode = result.GeneratedCode.ReadToEnd(); + + // this test assumes that if the IL to check the limit of the loop is present + // then the foreach was processed as an array access. + Assert.That( + cecilifiedCode, + Does.Match(""" + (il_topLevelMain_\d+\.)Append\(nop_12\); + (\s+\1Emit\(OpCodes\.)Ldloc, l_index_11\); + \2Ldloc, l_array_9\); + \2Ldlen\); + \2Conv_I4\); + \2Blt, ldloc_13\); + """), + "Array loop index check code does not match"); + + } + [Test] public void IDisposableStructEnumerator() { diff --git a/Cecilifier.Core/AST/StatementVisitor.ForEach.cs b/Cecilifier.Core/AST/StatementVisitor.ForEach.cs index c941eeb2..50a9a917 100644 --- a/Cecilifier.Core/AST/StatementVisitor.ForEach.cs +++ b/Cecilifier.Core/AST/StatementVisitor.ForEach.cs @@ -15,43 +15,78 @@ public override void VisitForEachStatement(ForEachStatementSyntax node) ExpressionVisitor.Visit(Context, _ilVar, node.Expression); var enumerableType = Context.GetTypeInfo(node.Expression).Type.EnsureNotNull(); - var getEnumeratorMethod = GetEnumeratorMethodFor(enumerableType); - var enumeratorType = EnumeratorTypeFor(getEnumeratorMethod); - - var enumeratorMoveNextMethod = MoveNextMethodFor(enumeratorType); - var enumeratorCurrentMethod = CurrentMethodFor(enumeratorType); - - var isDisposable = enumeratorType.Interfaces.FirstOrDefault(candidate => SymbolEqualityComparer.Default.Equals(candidate, Context.RoslynTypeSystem.SystemIDisposable)) != null; - - var context = new ForEachHandlerContext(getEnumeratorMethod, enumeratorCurrentMethod, enumeratorMoveNextMethod); - - // Get the enumerator.. - // we need to do this here (as opposed to in ProcessForEach() method) because we have a enumerable instance in the stack - // and if this enumerable implements IDisposable we'll emit a try/finally but it is not valid to enter try/finally blocks - // with a non empty stack. - Context.WriteNewLine(); - Context.WriteComment("variable to store the returned 'IEnumerator'."); - AddMethodCall(_ilVar, context.GetEnumeratorMethod); - context.EnumeratorVariableName = CodeGenerationHelpers.StoreTopOfStackInLocalVariable(Context, _ilVar, "enumerator", context.GetEnumeratorMethod.ReturnType).VariableName; + if (enumerableType.TypeKind == TypeKind.Array) + ProcessForEachOverArray(); + else + ProcessForEachOverEnumerable(); - if (isDisposable) + void ProcessForEachOverArray() { - ProcessWithInTryCatchFinallyBlock( - _ilVar, - context => - { - ProcessForEach((ForEachHandlerContext)context, node); - }, - Array.Empty(), - context => - { - ProcessForEachFinally((ForEachHandlerContext)context); - }, - context); + // save array in local variable... + var arrayVariable = CodeGenerationHelpers.StoreTopOfStackInLocalVariable(Context, _ilVar, "array", enumerableType); + + var loopVariable = CodeGenerationHelpers.AddLocalVariableToCurrentMethod(Context, node.Identifier.ValueText, Context.TypeResolver.Resolve(enumerableType.ElementTypeSymbolOf())).VariableName; + var loopIndexVar = CodeGenerationHelpers.AddLocalVariableToCurrentMethod(Context, "index", Context.TypeResolver.Resolve(Context.RoslynTypeSystem.SystemInt32)).VariableName; + + var conditionCheckLabelVar = CreateCilInstruction(_ilVar, OpCodes.Nop); + Context.EmitCilInstruction(_ilVar, OpCodes.Br, conditionCheckLabelVar); + var firstLoopBodyInstructionVar = CreateCilInstruction(_ilVar, OpCodes.Ldloc, arrayVariable.VariableName); + WriteCecilExpression(Context, $"{_ilVar}.Append({firstLoopBodyInstructionVar});"); + Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, loopIndexVar); + Context.EmitCilInstruction(_ilVar, enumerableType.ElementTypeSymbolOf().LdelemOpCode()); + Context.EmitCilInstruction(_ilVar, OpCodes.Stloc, loopVariable); + + // Loop body. + node.Statement.Accept(this); + + Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, loopIndexVar); + Context.EmitCilInstruction(_ilVar, OpCodes.Ldc_I4_1); + Context.EmitCilInstruction(_ilVar, OpCodes.Add); + Context.EmitCilInstruction(_ilVar, OpCodes.Stloc, loopIndexVar); + + // condition check... + WriteCecilExpression(Context, $"{_ilVar}.Append({conditionCheckLabelVar});"); + Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, loopIndexVar); + Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, arrayVariable.VariableName); + Context.EmitCilInstruction(_ilVar, OpCodes.Ldlen); + Context.EmitCilInstruction(_ilVar, OpCodes.Conv_I4); + Context.EmitCilInstruction(_ilVar, OpCodes.Blt, firstLoopBodyInstructionVar); } - else + + void ProcessForEachOverEnumerable() { - ProcessForEach(context, node); + var getEnumeratorMethod = GetEnumeratorMethodFor(enumerableType); + var enumeratorType = EnumeratorTypeFor(getEnumeratorMethod); + + var enumeratorMoveNextMethod = MoveNextMethodFor(enumeratorType); + var enumeratorCurrentMethod = CurrentMethodFor(enumeratorType); + + var isDisposable = enumeratorType.Interfaces.FirstOrDefault(candidate => SymbolEqualityComparer.Default.Equals(candidate, Context.RoslynTypeSystem.SystemIDisposable)) != null; + + var context = new ForEachHandlerContext(getEnumeratorMethod, enumeratorCurrentMethod, enumeratorMoveNextMethod); + + // Get the enumerator.. + // we need to do this here (as opposed to in ProcessForEach() method) because we have a enumerable instance in the stack + // and if this enumerable implements IDisposable we'll emit a try/finally but it is not valid to enter try/finally blocks + // with a non empty stack. + Context.WriteNewLine(); + Context.WriteComment("variable to store the returned 'IEnumerator'."); + AddMethodCall(_ilVar, context.GetEnumeratorMethod); + context.EnumeratorVariableName = CodeGenerationHelpers.StoreTopOfStackInLocalVariable(Context, _ilVar, "enumerator", context.GetEnumeratorMethod.ReturnType).VariableName; + + if (isDisposable) + { + ProcessWithInTryCatchFinallyBlock( + _ilVar, + context => ProcessForEach((ForEachHandlerContext) context, node), + Array.Empty(), + context => ProcessForEachFinally((ForEachHandlerContext) context), + context); + } + else + { + ProcessForEach(context, node); + } } } diff --git a/Cecilifier.Core/Extensions/TypeExtensions.cs b/Cecilifier.Core/Extensions/TypeExtensions.cs index cb390cce..3219f2db 100644 --- a/Cecilifier.Core/Extensions/TypeExtensions.cs +++ b/Cecilifier.Core/Extensions/TypeExtensions.cs @@ -130,6 +130,22 @@ public static OpCode StelemOpCode(this ITypeSymbol type) => _ => type.IsValueType ? OpCodes.Stelem_Any : throw new Exception($"Element type {type.Name} not supported.") }; + public static OpCode LdelemOpCode(this ITypeSymbol type) => + type.SpecialType switch + { + SpecialType.System_Byte => OpCodes.Ldelem_I1, + SpecialType.System_Char => OpCodes.Ldelem_I2, + SpecialType.System_Int16 => OpCodes.Ldelem_I2, + SpecialType.System_Int32 => OpCodes.Ldelem_I4, + SpecialType.System_Int64 => OpCodes.Ldelem_I8, + SpecialType.System_Single => OpCodes.Ldelem_R4, + SpecialType.System_Double => OpCodes.Ldelem_R8, + SpecialType.None => type.IsValueType ? OpCodes.Ldelem_Any : OpCodes.Ldelem_Ref, // Any => Custom structs, Ref => class. + SpecialType.System_String => OpCodes.Ldelem_Ref, + SpecialType.System_Object => OpCodes.Ldelem_Ref, + _ => type.IsValueType ? OpCodes.Ldelem_Any : throw new Exception($"Element type {type.Name} not supported.") + }; + public static bool IsTypeParameterOrIsGenericTypeReferencingTypeParameter(this ITypeSymbol returnType) => returnType.TypeKind == TypeKind.TypeParameter