Skip to content

Commit

Permalink
adds support for foreach on classes implementing the enumerable patte…
Browse files Browse the repository at this point in the history
…rn (part of #235)

the 'enumerator' pattern can be summarized as, give a type that implements:
1. a 'public bool MoveNext()' method and
2. has a public property named 'Current'

it can be used as the target of a foreach.
  • Loading branch information
adrianoc committed Jun 29, 2023
1 parent a7b8804 commit f047ab0
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 1 deletion.
43 changes: 43 additions & 0 deletions Cecilifier.Core.Tests/Tests/Unit/ForEachStatementTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Tests.Unit;

[TestFixture]
public class ForEachStatementTests : CecilifierUnitTestBase
{
// https://cutt.ly/swrhz6VE
//[TestCase("struct")]
[TestCase("sealed class")]
public void NonDisposableGetEnumeratorPattern(string enumeratorKind)
{
// Compiler uses GetEnumerator() method, does not require implementing IEnumerable<T>
var result = RunCecilifier($$"""
public {{enumeratorKind}} Enumerator
{
public int Current => 1;
public bool MoveNext() => false;
public Enumerator GetEnumerator() => default(Enumerator);
}
//TODO: change to top level statements when order of visiting of top level/classes gets fixed.
class Driver
{
static void Main()
{
foreach(var v in new Enumerator()) {}
}
}
""");
var cecilifiedCode = result.GeneratedCode.ReadToEnd();
Assert.That(cecilifiedCode, Does.Match("""
\s+//foreach\(var v in new Enumerator\(\)\) {}
\s+il_main_\d+.Emit\(OpCodes.Newobj, ctor_enumerator_\d+\);
"""), "enumerator type defined in the snippet should be used.");

Assert.That(cecilifiedCode, Does.Match("""
\s+//variable to store the returned 'IEnumerator<T>'.
\s+il_main_\d+.Emit\(OpCodes.Callvirt, m_getEnumerator_\d+\);
"""), "GetEnumerator() defined in the snippet should be used.");
}
}
80 changes: 80 additions & 0 deletions Cecilifier.Core/AST/StatementVisitor.ForEach.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Linq;
using Cecilifier.Core.Extensions;
using Cecilifier.Core.Misc;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Mono.Cecil.Cil;

namespace Cecilifier.Core.AST
{
internal partial class StatementVisitor
{
public override void VisitForEachStatement(ForEachStatementSyntax node)
{
ExpressionVisitor.Visit(Context, _ilVar, node.Expression);

var forEachTargetType = Context.GetTypeInfo(node.Expression).Type.EnsureNotNull();

var getEnumeratorMethod = forEachTargetType.GetMembers("GetEnumerator").OfType<IMethodSymbol>().Single();
var enumeratorType = EnumeratorTypeFor(getEnumeratorMethod);
var moveNextMethod = enumeratorType.GetMembers("MoveNext").Single().EnsureNotNull<ISymbol, IMethodSymbol>();
var currentGetter = getEnumeratorMethod.ReturnType.GetMembers("get_Current").Single().EnsureNotNull<ISymbol, IMethodSymbol>();

// Adds a variable to store current value in the foreach loop.
Context.WriteNewLine();
Context.WriteComment("variable to store current value in the foreach loop.");
var foreachCurrentValueVarName = CodeGenerationHelpers.AddLocalVariableToCurrentMethod(Context, node.Identifier.ValueText, Context.TypeResolver.Resolve(currentGetter.GetMemberType())).VariableName;

// Get the enumerator..
Context.WriteNewLine();
Context.WriteComment("variable to store the returned 'IEnumerator<T>'.");
AddMethodCall(_ilVar, getEnumeratorMethod);
var enumeratorVariableName = CodeGenerationHelpers.StoreTopOfStackInLocalVariable(Context, _ilVar, "enumerator", getEnumeratorMethod.ReturnType).VariableName;

var endOfLoopLabelVar = Context.Naming.Label("endForEach");
CreateCilInstruction(_ilVar, endOfLoopLabelVar, OpCodes.Nop);

// loop while enumerable.MoveNext() == true

var forEachLoopBegin = AddCilInstructionWithLocalVariable(_ilVar, OpCodes.Nop);

Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, enumeratorVariableName);
AddMethodCall(_ilVar, moveNextMethod);
Context.EmitCilInstruction(_ilVar, OpCodes.Brfalse, endOfLoopLabelVar);

Context.EmitCilInstruction(_ilVar, OpCodes.Ldloc, enumeratorVariableName);
AddMethodCall(_ilVar, currentGetter);
Context.EmitCilInstruction(_ilVar, OpCodes.Stloc, foreachCurrentValueVarName);

// process body of foreach
Context.WriteNewLine();
Context.WriteComment("foreach body");
node.Statement.Accept(this);
Context.WriteComment("end of foreach body");
Context.WriteNewLine();

Context.EmitCilInstruction(_ilVar, OpCodes.Br, forEachLoopBegin);
Context.WriteNewLine();
Context.WriteComment("end of foreach loop");
Context.WriteCecilExpression($"{_ilVar}.Append({endOfLoopLabelVar});");
Context.WriteNewLine();
}

public override void VisitForEachVariableStatement(ForEachVariableStatementSyntax node)
{
base.VisitForEachVariableStatement(node);
}

/*
* either the type returned by GetEnumerator() implements `IEnumerator` interface *or*
* it abides to the enumerator patterns, i.e, it has the following members:
* 1. public bool MoveNext() method
* 2. public T Current property ('T' can be any type)
*/
private ITypeSymbol EnumeratorTypeFor(IMethodSymbol getEnumeratorMethod)
{
var enumeratorType = getEnumeratorMethod.ReturnType.Interfaces.SingleOrDefault(itf => itf.Name == "IEnumerator");
return enumeratorType ?? getEnumeratorMethod.ReturnType;
}
}
}
1 change: 0 additions & 1 deletion Cecilifier.Core/AST/StatementVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,6 @@ void FinallyBlockHandler(string finallyEndVar)
}

public override void VisitLocalFunctionStatement(LocalFunctionStatementSyntax node) => node.Accept(new MethodDeclarationVisitor(Context));
public override void VisitForEachStatement(ForEachStatementSyntax node) { LogUnsupportedSyntax(node); }
public override void VisitWhileStatement(WhileStatementSyntax node) { LogUnsupportedSyntax(node); }
public override void VisitLockStatement(LockStatementSyntax node) { LogUnsupportedSyntax(node); }
public override void VisitUnsafeStatement(UnsafeStatementSyntax node) { LogUnsupportedSyntax(node); }
Expand Down
1 change: 1 addition & 0 deletions Cecilifier.Core/AST/SyntaxWalkerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ protected void AddMethodCall(string ilVar, IMethodSymbol method, bool isAccessOn
}
else
{
EnsureForwardedMethod(Context, method, Array.Empty<TypeParameterSyntax>());
var operand = method.MethodResolverExpression(Context);
Context.EmitCilInstruction(ilVar, opCode, operand);
}
Expand Down

0 comments on commit f047ab0

Please sign in to comment.