Browse Source

Fix #3804: reconstruct foreach over an inline array

The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.

Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.

Assisted-by: Claude:claude-fable-5:Claude Code
fix-3860-outvar-explicit-type
Siegfried Pammer 2 days ago committed by Siegfried Pammer
parent
commit
f380b31a27
  1. 10
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs
  2. 109
      ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs

10
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs

@ -99,6 +99,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return array[GetIndex()] = (array[GetIndex() + 1] = value); 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() public void OverloadResolution()
{ {
Receiver(GetByte16()); Receiver(GetByte16());

109
ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs

@ -93,6 +93,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public override AstNode VisitForStatement(ForStatement forStatement) public override AstNode VisitForStatement(ForStatement forStatement)
{ {
AstNode? result = TransformForeachOnArray(forStatement); AstNode? result = TransformForeachOnArray(forStatement);
if (result != null)
return result;
result = TransformForeachOnInlineArray(forStatement);
if (result != null) if (result != null)
return result; return result;
return base.VisitForStatement(forStatement); return base.VisitForStatement(forStatement);
@ -402,6 +405,112 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return foreachStmt; 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"))
}
}
};
/// <summary>
/// Reconstructs a <c>foreach</c> over an inline array from the <c>for</c> loop the compiler
/// lowers it to: <c>for (i = 0; i &lt; N; i++) { item = &lt;PrivateImplementationDetails&gt;.InlineArrayElementRef(ref buffer, i); ... }</c>.
/// The rewrite is only sound because the loop bound <c>N</c> equals the inline array length,
/// which proves the index is always in range: <c>InlineArrayElementRef</c> is the compiler's
/// unchecked element accessor, whereas the C# inline-array indexer <c>buffer[i]</c> 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.
/// </summary>
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<IdentifierExpression>("itemVariable").Single().GetILVariable();
var indexVariable = m.Get<IdentifierExpression>("indexVariable").Single().GetILVariable();
if (itemVariable == null || indexVariable == null)
return null;
// The loop body must start with `item = InlineArrayElementRef(ref buffer, index)`.
if (m.Get<Expression>("elementAccess").Single() is not InvocationExpression elementAccess)
return null;
if (elementAccess.GetSymbol() is not IMethod { DeclaringType.FullName: "<PrivateImplementationDetails>" } 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<PrimitiveExpression>("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 = <PrivateImplementationDetails>.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 { static readonly ForStatement forOnArrayMultiDimPattern = new ForStatement {
Initializers = { }, Initializers = { },
Condition = new BinaryOperatorExpression( Condition = new BinaryOperatorExpression(

Loading…
Cancel
Save