mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
#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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user