Skip to content

Commit

Permalink
adds support for handling arrays in foreach (part of #235)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianoc committed Jul 22, 2023
1 parent a7cd1e3 commit ae2ef37
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 34 deletions.
28 changes: 27 additions & 1 deletion Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
101 changes: 68 additions & 33 deletions Cecilifier.Core/AST/StatementVisitor.ForEach.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>'.");
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<CatchClauseSyntax>(),
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<T>'.");
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<CatchClauseSyntax>(),
context => ProcessForEachFinally((ForEachHandlerContext) context),
context);
}
else
{
ProcessForEach(context, node);
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions Cecilifier.Core/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ae2ef37

Please sign in to comment.