#4019: transform value.<Clone>$() into value with {} even if there are no initializers.

This commit also changes the ILAst representation of with-initializers to preserve the `<Clone>$()` call, so that BlockKind.WithInitializer only forces the block into a certain shape, without assigning its special semantics.
This commit is contained in:
Daniel Grunwald
2026-10-11 14:05:15 +02:00
parent 6a5ecc5641
commit fa863a8229
6 changed files with 92 additions and 32 deletions
@@ -129,6 +129,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
};
}
public Fields EmptyWith(Fields input)
{
return input with { };
}
}
public abstract record WithNestedRecords
@@ -4051,13 +4051,22 @@ namespace ICSharpCode.Decompiler.CSharp
TranslatedExpression TranslateWithInitializer(Block block)
{
var stloc = block.Instructions.FirstOrDefault() as StLoc;
var final = block.FinalInstruction as LdLoc;
if (stloc == null || final == null || stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget)
if (block.Instructions.FirstOrDefault() is not StLoc stloc
|| block.FinalInstruction is not LdLoc final
|| stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget)
{
throw new ArgumentException("given Block is invalid!");
}
ILInstruction oldValue = stloc.Value;
if (stloc.Variable.Type.IsReferenceType == true)
{
// reference types must involve a <Clone>$() call
Debug.Assert(TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall((CallInstruction)stloc.Value));
oldValue = ((CallInstruction)stloc.Value).Arguments.Single();
}
WithInitializerExpression withInitializerExpression = new WithInitializerExpression();
withInitializerExpression.Expression = Translate(stloc.Value, stloc.Variable.Type);
withInitializerExpression.Expression = Translate(oldValue, stloc.Variable.Type);
withInitializerExpression.Initializer = BuildArrayInitializerExpression(block, new InitializedObjectResolveResult(stloc.Variable.Type));
return withInitializerExpression.WithILInstruction(block)
@@ -1339,6 +1339,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
{
StartNode(withInitializerExpression);
withInitializerExpression.Expression.AcceptVisitor(this);
Space();
WriteKeyword("with");
withInitializerExpression.Initializer.AcceptVisitor(this);
EndNode(withInitializerExpression);
+35 -23
View File
@@ -171,6 +171,7 @@ namespace ICSharpCode.Decompiler.IL
break;
case BlockKind.CollectionInitializer:
case BlockKind.ObjectInitializer:
case BlockKind.WithInitializer:
var final2 = finalInstruction as LdLoc;
Debug.Assert(final2 != null);
var initVar2 = final2!.Variable;
@@ -178,30 +179,41 @@ namespace ICSharpCode.Decompiler.IL
IType? type2 = null;
bool condition = Instructions[0].MatchStLoc(final2.Variable, out var init2);
Debug.Assert(condition);
Debug.Assert(init2 is NewObj
|| init2 is DefaultValue
|| (init2 is CallInstruction c && c.Method.FullNameIs("System.Activator", "CreateInstance") && c.Method.TypeArguments.Count == 1)
|| (init2 is Block named && named.Kind == BlockKind.CallWithNamedArgs));
switch (init2)
if (Kind == BlockKind.WithInitializer)
{
case NewObj newObj:
type2 = newObj.Method.DeclaringType;
break;
case DefaultValue defaultValue:
type2 = defaultValue.Type;
break;
case Block callWithNamedArgs when callWithNamedArgs.Kind == BlockKind.CallWithNamedArgs:
type2 = ((CallInstruction)callWithNamedArgs.FinalInstruction).Method.ReturnType;
break;
case CallInstruction ci2 when TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall(ci2):
type2 = ci2.Method.DeclaringType;
break;
case Call c2 when c2.Method.FullNameIs("System.Activator", "CreateInstance") && c2.Method.TypeArguments.Count == 1:
type2 = c2.Method.TypeArguments[0];
break;
default:
Debug.Assert(false);
break;
if (initVar2.Type.IsReferenceType == true)
{
Debug.Assert(init2 is CallInstruction cloneCall && TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall(cloneCall));
}
type2 = initVar2.Type;
}
else
{
Debug.Assert(init2 is NewObj
|| init2 is DefaultValue
|| (init2 is CallInstruction c && c.Method.FullNameIs("System.Activator", "CreateInstance") && c.Method.TypeArguments.Count == 1)
|| (init2 is Block named && named.Kind == BlockKind.CallWithNamedArgs));
switch (init2)
{
case NewObj newObj:
type2 = newObj.Method.DeclaringType;
break;
case DefaultValue defaultValue:
type2 = defaultValue.Type;
break;
case Block callWithNamedArgs when callWithNamedArgs.Kind == BlockKind.CallWithNamedArgs:
type2 = ((CallInstruction)callWithNamedArgs.FinalInstruction).Method.ReturnType;
break;
case CallInstruction ci2 when TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall(ci2):
type2 = ci2.Method.DeclaringType;
break;
case Call c2 when c2.Method.FullNameIs("System.Activator", "CreateInstance") && c2.Method.TypeArguments.Count == 1:
type2 = c2.Method.TypeArguments[0];
break;
default:
Debug.Assert(false);
break;
}
}
for (int i = 1; i < Instructions.Count; i++)
{
@@ -318,13 +318,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
return;
}
TransformAssignment.HandleCompoundAssign(inst, context);
if (TransformAssignment.HandleCompoundAssign(inst, context))
return;
if (TransformCollectionAndObjectInitializers.HandleEmptyWith(inst, context))
return;
}
protected internal override void VisitCallVirt(CallVirt inst)
{
base.VisitCallVirt(inst);
TransformAssignment.HandleCompoundAssign(inst, context);
if (TransformAssignment.HandleCompoundAssign(inst, context))
return;
if (TransformCollectionAndObjectInitializers.HandleEmptyWith(inst, context))
return;
}
protected internal override void VisitNewObj(NewObj inst)
@@ -34,6 +34,32 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// </summary>
public class TransformCollectionAndObjectInitializers : IStatementTransform
{
/// <summary>
/// Handles 'with' expressions with 0 initializers.
/// These do not start with the `stloc` that this transform typically looks for,
/// so they are handled separately as part of ExpressionTransforms.
/// </summary>
public static bool HandleEmptyWith(CallInstruction inst, ILTransformContext context)
{
if (!context.Settings.WithExpressions)
return false;
if (inst.Parent is StLoc { Parent: Block { Kind: BlockKind.WithInitializer } })
return false; // already transformed
if (!IsRecordCloneMethodCall(inst))
return false;
var parentChildren = inst.Parent!.Children;
int insertionIndex = inst.ChildIndex;
context.Step("<Clone>$ to with-expression", inst);
var initializerBlock = new Block(BlockKind.WithInitializer);
ILVariable finalSlot = context.Function.RegisterVariable(VariableKind.InitializerTarget, inst.InferType(context.TypeSystem));
initializerBlock.FinalInstruction = new LdLoc(finalSlot);
initializerBlock.Instructions.Add(new StLoc(finalSlot, inst));
parentChildren[insertionIndex] = initializerBlock;
context.EndStep(inst);
// The with-initializer block might later get extended with additional initializers by Run().
return true;
}
void IStatementTransform.Run(Block block, int pos, StatementTransformContext context)
{
if (!context.Settings.ObjectOrCollectionInitializers)
@@ -93,10 +119,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
instType = c.Method.TypeArguments[0];
blockKind = BlockKind.ObjectInitializer;
break;
case CallInstruction ci when context.Settings.WithExpressions && IsRecordCloneMethodCall(ci):
instType = ci.Method.DeclaringType;
case Block { Kind: BlockKind.WithInitializer, Instructions: [StLoc { Value: var existingInitInst }] }:
// Extend existing with-initializer block that was created by HandleEmptyWith.
instType = existingInitInst.InferType(context.TypeSystem);
blockKind = BlockKind.WithInitializer;
initInst = ci.Arguments.Single();
initInst = existingInitInst;
break;
default:
var typeDef = v.Type.GetDefinition();