diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs index 57049c08e..537db46ad 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs @@ -99,6 +99,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty return array[GetIndex()] = (array[GetIndex() + 1] = value); } + public int SumForeach(Byte16 b) + { + int num = 0; + foreach (byte b2 in b) + { + num += b2; + } + return num; + } + public void OverloadResolution() { Receiver(GetByte16()); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index a321808ff..585df5049 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -93,6 +93,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public override AstNode VisitForStatement(ForStatement forStatement) { AstNode? result = TransformForeachOnArray(forStatement); + if (result != null) + return result; + result = TransformForeachOnInlineArray(forStatement); if (result != null) return result; return base.VisitForStatement(forStatement); @@ -402,6 +405,112 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return foreachStmt; } + static readonly ForStatement forOnInlineArrayPattern = new ForStatement { + Initializers = { + new ExpressionStatement( + new AssignmentExpression( + new NamedNode("indexVariable", new IdentifierExpression(Pattern.AnyString)), + new PrimitiveExpression(0) + )) + }, + Condition = new BinaryOperatorExpression( + new IdentifierExpressionBackreference("indexVariable"), + BinaryOperatorType.LessThan, + new NamedNode("length", new PrimitiveExpression(PrimitiveExpression.AnyValue)) + ), + Iterators = { + new ExpressionStatement( + new AssignmentExpression( + new IdentifierExpressionBackreference("indexVariable"), + new BinaryOperatorExpression(new IdentifierExpressionBackreference("indexVariable"), BinaryOperatorType.Add, new PrimitiveExpression(1)) + )) + }, + EmbeddedStatement = new BlockStatement { + Statements = { + new ExpressionStatement(new AssignmentExpression( + new NamedNode("itemVariable", new IdentifierExpression(Pattern.AnyString)), + new NamedNode("elementAccess", new AnyNode()) + )), + new Repeat(new AnyNode("statements")) + } + } + }; + + /// + /// Reconstructs a foreach over an inline array from the for loop the compiler + /// lowers it to: for (i = 0; i < N; i++) { item = <PrivateImplementationDetails>.InlineArrayElementRef(ref buffer, i); ... }. + /// The rewrite is only sound because the loop bound N equals the inline array length, + /// which proves the index is always in range: InlineArrayElementRef is the compiler's + /// unchecked element accessor, whereas the C# inline-array indexer buffer[i] is + /// bounds-checked, so the two only agree when the index is provably in-bounds. A loop that + /// does not match this exact shape keeps the (unnameable but faithful) helper call. + /// + Statement? TransformForeachOnInlineArray(ForStatement forStatement) + { + if (!context.Settings.ForEachStatement || !context.Settings.InlineArrays) + return null; + Match m = forOnInlineArrayPattern.Match(forStatement); + if (!m.Success) + return null; + var itemVariable = m.Get("itemVariable").Single().GetILVariable(); + var indexVariable = m.Get("indexVariable").Single().GetILVariable(); + if (itemVariable == null || indexVariable == null) + return null; + + // The loop body must start with `item = InlineArrayElementRef(ref buffer, index)`. + if (m.Get("elementAccess").Single() is not InvocationExpression elementAccess) + return null; + if (elementAccess.GetSymbol() is not IMethod { DeclaringType.FullName: "" } helper) + return null; + if (helper.Name is not ("InlineArrayElementRef" or "InlineArrayElementRefReadOnly")) + return null; + if (elementAccess.Arguments.Count != 2) + return null; + // arg0: `ref buffer`, arg1: the loop index. + if (elementAccess.Arguments.First() is not DirectionExpression { Expression: IdentifierExpression bufferIdentifier }) + return null; + var bufferVariable = bufferIdentifier.GetILVariable(); + if (bufferVariable == null) + return null; + if (elementAccess.Arguments.Last() is not IdentifierExpression indexIdentifier + || indexIdentifier.GetILVariable() != indexVariable) + return null; + + // Soundness: the loop counts 0..length-1 over exactly the inline array's length, so the + // index is provably in range. Any other bound (or a non-inline-array buffer) is rejected. + if (bufferVariable.Type.GetInlineArrayLength() is not int arrayLength) + return null; + if (m.Get("length").Single().Value is not int loopBound || loopBound != arrayLength) + return null; + + if (!VariableCanBeUsedAsForeachLocal(itemVariable, forStatement)) + return null; + // The index is a pure counter: stored at init + increment, loaded at the condition, + // the increment, and the element access; never captured by address. + if (indexVariable.StoreCount != 2 || indexVariable.LoadCount != 3 || indexVariable.AddressCount != 0) + return null; + + context.Step("Introduce foreach over inline array", forStatement); + // Take the buffer reference for the `in` expression before dropping the element access. + var inExpression = bufferIdentifier.Detach(); + // Reuse the loop body (preserving its annotations) after removing its leading + // `item = .InlineArrayElementRef(ref buffer, i)` statement. + var body = (BlockStatement)forStatement.EmbeddedStatement; + body.Statements.First().Remove(); + var foreachStmt = new ForeachStatement { + VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type), + VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name! }, + InExpression = inExpression, + EmbeddedStatement = body.Detach() + }; + foreachStmt.CopyAnnotationsFrom(forStatement); + itemVariable.Kind = IL.VariableKind.ForeachLocal; + foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); + forStatement.ReplaceWith(foreachStmt); + context.EndStep(foreachStmt); + return foreachStmt; + } + static readonly ForStatement forOnArrayMultiDimPattern = new ForStatement { Initializers = { }, Condition = new BinaryOperatorExpression(