diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs index 3a4f266d78..9c14435cbc 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs @@ -45,6 +45,13 @@ public static void Test() } } + public class Issue3827Node + { + public Issue3827Node next; + + public int value; + } + public delegate ref T RefFunc(); public delegate ref readonly T ReadOnlyRefFunc(); public delegate ref TReturn RefFunc(T1 param1); @@ -159,6 +166,28 @@ public void RefReadonlyCallVirt(RefLocalsAndReturns provider) private static int DefaultInt = 0; + public static Issue3827Node RefLocalUsedAfterForLoop(Issue3827Node[] buckets, int hash, Issue3827Node newNode) + { + // The ref local is used after the loop, so its declaration is hoisted in front of the loop. + // The loop is kept as a while-loop (rather than converted to a headless for-loop) so the + // ref-assignment stays on the declaration -- a ref local cannot be declared without an initializer. + ref Issue3827Node reference = ref buckets[hash & 3]; + while (reference != null) + { + if (reference.value == hash) + { + reference = newNode; + break; + } + reference = ref reference.next; + } + if (reference == null) + { + reference = newNode; + } + return reference; + } + public static ref T GetRef() { throw new NotImplementedException(); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index 2aa550794c..15e25c13d8 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -202,6 +202,12 @@ public override AstNode VisitTryCatchStatement(TryCatchStatement tryCatchStateme if (variable != m3.Get("ident").Single().GetILVariable()) return null; WhileStatement loop = (WhileStatement)next; + // Cannot convert to for loop, if the iteration variable is a ref local used after the loop: its + // declaration is hoisted in front, leaving a headless `for (; cond; v = ref ...)` whose only + // initialization is the for-initializer ref-assignment -- which can't be split from a ref local + // (CS8174). Keeping it a while-loop matches the source and keeps the initializer on the decl. + if (variable != null && variable.Type.IsByRefLike && IsVariableUsedAfter(loop, variable)) + return null; // Cannot convert to for loop, if any variable that is used in the "iterator" part of the pattern, // will be declared in the body of the while-loop. var iteratorStatement = m3.Get("iterator").Single(); @@ -244,6 +250,16 @@ bool ForStatementUsesVariable(ForStatement statement, IL.ILVariable? variable) return false; } + bool IsVariableUsedAfter(Statement loop, IL.ILVariable variable) + { + for (AstNode? sibling = loop.NextSibling; sibling != null; sibling = sibling.NextSibling) + { + if (sibling.DescendantsAndSelf.OfType().Any(ie => ie.GetILVariable() == variable)) + return true; + } + return false; + } + bool IteratorVariablesDeclaredInsideLoopBody(Statement iteratorStatement) { foreach (var id in iteratorStatement.DescendantsAndSelf.OfType()) diff --git a/ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs index 78e4138266..83000fe100 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs @@ -395,6 +395,8 @@ bool MatchForLoop(BlockContainer loop, IfInstruction whileCondition, Block while // - increment block if (incrementBlock.Instructions.Count <= 1 || loop.Blocks.Count < 3) return false; + if (StoresRefLocalUsedAfterLoop(loop, incrementBlock)) + return false; context.Step("Transform to for loop: " + loop.EntryPoint.Label, loop); // move the block to the end of the loop: loop.Blocks.MoveElementToEnd(incrementBlock); @@ -467,6 +469,27 @@ bool MatchForLoop(BlockContainer loop, IfInstruction whileCondition, Block while return true; } + static bool StoresRefLocalUsedAfterLoop(BlockContainer loop, Block incrementBlock) + { + foreach (var store in incrementBlock.Instructions.SkipLast(1).SelectMany(inst => inst.Descendants).OfType()) + { + var variable = store.Variable; + if (variable.Type.IsByRefLike && IsVariableUsedAfterLoop(loop, variable)) + return true; + } + return false; + } + + static bool IsVariableUsedAfterLoop(BlockContainer loop, ILVariable variable) + { + foreach (var use in variable.LoadInstructions.Concat(variable.AddressInstructions).Concat(variable.StoreInstructions.Cast())) + { + if (loop.GetCommonParent(use) is Block { Kind: BlockKind.ControlFlow } && loop.IsBefore(use)) + return true; + } + return false; + } + bool IsAssignment(ILInstruction inst) { if (inst is StLoc)