Browse Source

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
pull/3863/head
Sebastien Lebreton 2 weeks ago committed by Siegfried Pammer
parent
commit
13c80c4588
  1. 28
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs
  2. 59
      ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs

28
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<T>(); public delegate ref T RefFunc<T>();
public delegate ref readonly T ReadOnlyRefFunc<T>(); public delegate ref readonly T ReadOnlyRefFunc<T>();
public delegate ref TReturn RefFunc<T1, TReturn>(T1 param1); public delegate ref TReturn RefFunc<T1, TReturn>(T1 param1);
@ -159,6 +166,27 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
private static int DefaultInt = 0; 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<T>() public static ref T GetRef<T>()
{ {
throw new NotImplementedException(); throw new NotImplementedException();

59
ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs

@ -588,6 +588,39 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
&& identExpr.TypeArguments.Count == 0; && identExpr.TypeArguments.Count == 0;
} }
/// <summary>
/// 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).
/// </summary>
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) bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext context)
{ {
if (v.Type.IsByRefLike) if (v.Type.IsByRefLike)
@ -641,6 +674,32 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return vds; return vds;
}, "Combine variable declaration with initializer")); }, "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)) else if (CanBeDeclaredAsOutVariable(v, out var dirExpr))
{ {
// 'T v; SomeCall(out v);' can be combined to 'SomeCall(out T v);' // 'T v; SomeCall(out v);' can be combined to 'SomeCall(out T v);'

Loading…
Cancel
Save