From 13c80c4588971c759cfe4804b2955aa72203e513 Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Fri, 26 Jun 2026 13:32:14 +0200 Subject: [PATCH] Fix #3827: keep the initializer on a ref local hoisted out of a for-loop A ref local that is used after a for-loop has its declaration hoisted in front of the loop, but its only initialization is the for-initializer ref-assignment. The declaration was then emitted without an initializer (`ref T x;`), which does not compile (CS8174). When a by-ref-like local's matching assignment is the first for-initializer, move the ref-assignment's value up into the declaration (`ref T x = ref expr;`) and drop the for-initializer. Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI --- .../TestCases/Pretty/RefLocalsAndReturns.cs | 28 +++++++++ .../CSharp/Transforms/DeclareVariables.cs | 59 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs index 3a4f266d7..ccf2ddb5b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs @@ -45,6 +45,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + 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,27 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty 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 + // for-statement; the for-initializer ref-assignment must stay on the declaration, because + // a ref local cannot be declared without an initializer. + ref Issue3827Node reference = ref buckets[hash & 3]; + for (; reference != null; reference = ref reference.next) + { + if (reference.value == hash) + { + reference = newNode; + break; + } + } + if (reference == null) + { + reference = newNode; + } + return reference; + } + public static ref T GetRef() { throw new NotImplementedException(); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index 8fbc4b9a3..9fc32bda2 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -588,6 +588,39 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms && identExpr.TypeArguments.Count == 0; } + /// + /// Matches the case where the variable's only initialization is the first initializer of a + /// for-statement (`for (v = expr; ...)`) and the declaration has to be placed in front of the + /// for-statement (e.g. because the variable is used after the loop). Used to keep a ref local's + /// initializer on its declaration (a ref local may not be declared without one). + /// + bool IsMatchingForInitializerAssignment(VariableToDeclare v, [NotNullWhen(true)] out ForStatement? forStatement, + [NotNullWhen(true)] out Statement? initializerStatement, [NotNullWhen(true)] out AssignmentExpression? assignment) + { + forStatement = null; + initializerStatement = null; + assignment = null; + + if (v.InsertionPoint.nextNode is not ForStatement { Parent: BlockStatement } fs) + return false; + + if (fs.Initializers.FirstOrDefault() is not ExpressionStatement { Expression: AssignmentExpression a } firstInit) + return false; + + if (a.Operator != AssignmentOperatorType.Assign + || a.Left is not IdentifierExpression identExpr + || identExpr.Identifier != v.Name + || identExpr.TypeArguments.Count != 0) + { + return false; + } + + forStatement = fs; + initializerStatement = firstInit; + assignment = a; + return true; + } + bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext context) { if (v.Type.IsByRefLike) @@ -641,6 +674,32 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return vds; }, "Combine variable declaration with initializer")); } + else if (v.Type.IsByRefLike && IsMatchingForInitializerAssignment(v, out var forStatement, out var forInitializerStatement, out var forInitAssignment)) + { + // A by-ref-like local that is used after the loop has its declaration hoisted in + // front of the for-statement, but its only initialization is the for-initializer + // ref-assignment. A ref local must be declared with an initializer (CS8174), so move + // the ref-assignment's value up into the declaration and drop the for-initializer. + AstType type = context.TypeSystemAstBuilder.ConvertType(v.Type); + if (v.ILVariable.IsRefReadOnly && type is ComposedType { HasRefSpecifier: true } composedType) + { + composedType.HasReadOnlySpecifier = true; + } + + var vds = new VariableDeclarationStatement(type, v.Name, forInitAssignment.Right.Detach()); + var init = vds.Variables.Single(); + init.AddAnnotation(forInitAssignment.Left.GetResolveResult()); + foreach (object annotation in forInitAssignment.Left.Annotations.Concat(forInitAssignment.Annotations)) + { + if (annotation is not ResolveResult) + { + init.AddAnnotation(annotation); + } + } + + forInitializerStatement.Remove(); + forStatement.Parent!.InsertChildBefore(forStatement, vds, Slots.Statement); + } else if (CanBeDeclaredAsOutVariable(v, out var dirExpr)) { // 'T v; SomeCall(out v);' can be combined to 'SomeCall(out T v);'