Files
ILSpy/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs
T
Siegfried Pammer 565e1a3442 Handle legacy VB delegate wrappers
VB can lower event-handler closures through generated wrapper methods, closure copy constructors, and immediately invoked VB anonymous delegate helpers. Recognizing and unwrapping those shapes leaves the expected lambda form and lets captured locals be scalar-replaced instead of exposing generated closure artifacts.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-08 17:52:13 +02:00

998 lines
34 KiB
C#

// Copyright (c) 2019 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.IL.ControlFlow;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
namespace ICSharpCode.Decompiler.IL.Transforms
{
/// <summary>
/// Transforms closure fields to local variables.
///
/// This is a post-processing step of <see cref="DelegateConstruction"/>, <see cref="LocalFunctionDecompiler"/>
/// and <see cref="TransformExpressionTrees"/>.
///
/// In general we can perform SROA (scalar replacement of aggregates) on any variable that
/// satisfies the following conditions:
/// 1) It is initialized by an empty/default constructor call.
/// 2) The variable is never passed to another method.
/// 3) The variable is never the target of an invocation.
///
/// Note that 2) and 3) apply because declarations and uses of lambdas and local functions
/// are already transformed by the time this transform is applied.
/// </summary>
public class TransformDisplayClassUsage : ILVisitor, IILTransform
{
class VariableToDeclare
{
private readonly DisplayClass container;
private readonly IField field;
private ILVariable declaredVariable;
public string Name => @field.Name;
public bool CanPropagate { get; private set; }
public bool UsesInitialValue { get; set; }
public HashSet<ILInstruction> Initializers { get; } = new HashSet<ILInstruction>();
public VariableToDeclare(DisplayClass container, IField field, ILVariable declaredVariable = null)
{
this.container = container;
this.field = field;
this.declaredVariable = declaredVariable;
Debug.Assert(declaredVariable == null || declaredVariable.StateMachineField == field);
}
public void Propagate(ILVariable variable)
{
this.declaredVariable = variable;
this.CanPropagate = variable != null;
}
public ILVariable GetOrDeclare()
{
if (declaredVariable != null)
return declaredVariable;
declaredVariable = container.Variable.Function.RegisterVariable(VariableKind.Local, field.Type, field.Name);
declaredVariable.InitialValueIsInitialized = true;
declaredVariable.UsesInitialValue = UsesInitialValue;
declaredVariable.CaptureScope = container.CaptureScope;
return declaredVariable;
}
}
[DebuggerDisplay("[DisplayClass {Variable} : {Type}]")]
class DisplayClass
{
public readonly ILVariable Variable;
public readonly ITypeDefinition Type;
public readonly Dictionary<IField, VariableToDeclare> VariablesToDeclare;
public BlockContainer CaptureScope;
public ILInstruction Initializer;
public DisplayClass(ILVariable variable, ITypeDefinition type)
{
Variable = variable;
Type = type;
VariablesToDeclare = new Dictionary<IField, VariableToDeclare>();
}
}
ILTransformContext context;
ITypeResolveContext decompilationContext;
readonly Dictionary<ILVariable, DisplayClass> displayClasses = new Dictionary<ILVariable, DisplayClass>();
readonly Dictionary<ILVariable, ILVariable> displayClassCopyMap = new Dictionary<ILVariable, ILVariable>();
void IILTransform.Run(ILFunction function, ILTransformContext context)
{
if (this.context != null)
throw new InvalidOperationException("Reentrancy in " + nameof(TransformDisplayClassUsage));
try
{
this.context = context;
this.decompilationContext = new SimpleTypeResolveContext(context.Function.Method);
AnalyzeFunction(function);
Transform(function);
}
finally
{
ClearState();
}
}
void ClearState()
{
displayClasses.Clear();
displayClassCopyMap.Clear();
this.decompilationContext = null;
this.context = null;
}
void AnalyzeFunction(ILFunction function)
{
void VisitFunction(ILFunction f)
{
foreach (var v in f.Variables.ToArray())
{
var result = AnalyzeVariable(v);
if (result == null || displayClasses.ContainsKey(result.Variable))
continue;
context.Step($"Detected display-class {result.Variable}", result.Initializer ?? f.Body);
displayClasses.Add(result.Variable, result);
}
}
void VisitChildren(ILInstruction inst)
{
foreach (var child in inst.Children)
{
Visit(child);
}
}
void Visit(ILInstruction inst)
{
switch (inst)
{
case ILFunction f:
VisitFunction(f);
VisitChildren(inst);
break;
default:
VisitChildren(inst);
break;
}
}
Visit(function);
foreach (var (v, displayClass) in displayClasses.ToArray())
{
if (!ValidateDisplayClassUses(v, displayClass))
displayClasses.Remove(v);
}
foreach (var displayClass in displayClasses.Values)
{
// handle uninitialized fields
foreach (var f in displayClass.Type.Fields)
{
if (displayClass.VariablesToDeclare.ContainsKey(f))
continue;
var variable = AddVariable(displayClass, null, f);
variable.UsesInitialValue = true;
displayClass.VariablesToDeclare[(IField)f.MemberDefinition] = variable;
}
foreach (var v in displayClass.VariablesToDeclare.Values)
{
if (v.CanPropagate)
{
var variableToPropagate = v.GetOrDeclare();
if (variableToPropagate.Kind != VariableKind.Parameter && !displayClasses.ContainsKey(variableToPropagate))
v.Propagate(null);
}
}
}
}
bool ValidateDisplayClassUses(ILVariable v, DisplayClass displayClass)
{
foreach (var ldloc in v.LoadInstructions)
{
if (!ValidateUse(displayClass, ldloc))
return false;
}
foreach (var ldloca in v.AddressInstructions)
{
if (!ValidateUse(displayClass, ldloca))
return false;
}
return true;
bool ValidateUse(DisplayClass container, ILInstruction use)
{
IField field;
switch (use.Parent)
{
case LdFlda ldflda when ldflda.MatchLdFlda(out var target, out field) && target == use:
var keyField = (IField)field.MemberDefinition;
if (!container.VariablesToDeclare.TryGetValue(keyField, out VariableToDeclare variable) || variable == null)
{
variable = AddVariable(container, null, field);
}
if (variable.CanPropagate && !IsPlainReadOrInitializerStore(ldflda, variable))
{
// The field is mutated after initialization (stored again, or its address
// escapes). Propagation is only sound if the store can be redirected to
// the propagated variable: possible for parameters (their remaining uses
// are restricted in ResolveVariableToPropagate), but not for 'this' and
// not for a variable that is itself a scalar-replaced display class.
var propagatedVariable = variable.GetOrDeclare();
if (propagatedVariable.IsThis() || displayClasses.ContainsKey(propagatedVariable))
{
variable.Propagate(null);
}
}
container.VariablesToDeclare[keyField] = variable;
return true;
case StObj stobj when stobj.MatchStObj(out var target, out ILInstruction value, out _) && value == use:
if (target.MatchLdFlda(out var load, out field) && load.MatchLdLocRef(out var otherVariable) && displayClasses.TryGetValue(otherVariable, out var otherDisplayClass))
{
if (otherDisplayClass.VariablesToDeclare.TryGetValue((IField)field.MemberDefinition, out var declaredVar))
return declaredVar.CanPropagate;
}
return false;
case StLoc stloc when stloc.Variable.IsSingleDefinition && stloc.Value == use:
displayClassCopyMap[stloc.Variable] = v;
return ValidateDisplayClassUses(stloc.Variable, displayClass);
default:
return false;
}
}
}
/// <summary>
/// True when the given field access is a plain read or one of the recorded
/// initializer stores; false for any other store or for an escaping address.
/// </summary>
static bool IsPlainReadOrInitializerStore(LdFlda ldflda, VariableToDeclare variable)
{
switch (ldflda.Parent)
{
case LdObj:
return true;
case StObj store when store.Target == ldflda:
return variable.Initializers.Contains(store);
default:
return false;
}
}
private DisplayClass AnalyzeVariable(ILVariable v)
{
switch (v.Kind)
{
case VariableKind.Parameter:
if (context.Settings.YieldReturn && v.Function.StateMachineCompiledWithMono && v.IsThis())
return HandleMonoStateMachine(v.Function, v);
return null;
case VariableKind.StackSlot:
case VariableKind.Local:
case VariableKind.DisplayClassLocal:
return DetectDisplayClass(v);
case VariableKind.InitializerTarget:
return DetectDisplayClassInitializer(v);
default:
return null;
}
}
DisplayClass DetectDisplayClass(ILVariable v)
{
ITypeDefinition definition;
if (v.Kind != VariableKind.StackSlot)
{
definition = v.Type.GetDefinition();
}
else if (v.StoreInstructions.Count > 0 && v.StoreInstructions[0] is StLoc stloc)
{
definition = stloc.Value.InferType(context.TypeSystem).GetDefinition();
}
else
{
definition = null;
}
if (!ValidateDisplayClassDefinition(definition))
return null;
DisplayClass result;
switch (definition.Kind)
{
case TypeKind.Class:
if (!v.IsSingleDefinition)
return null;
if (!(v.StoreInstructions.SingleOrDefault() is StLoc stloc))
return null;
if (stloc.Value is NewObj newObj && ValidateConstructor(context, newObj.Method))
{
result = new DisplayClass(v, definition) {
CaptureScope = v.CaptureScope,
Initializer = stloc
};
}
else
{
return null;
}
HandleInitBlock(stloc.Parent as Block, stloc.ChildIndex + 1, result, result.Variable);
break;
case TypeKind.Struct:
if (v.StoreInstructions.Count != 0)
return null;
Debug.Assert(v.StoreInstructions.Count == 0);
result = new DisplayClass(v, definition) { CaptureScope = v.CaptureScope };
HandleInitBlock(FindDisplayStructInitBlock(v), 0, result, result.Variable);
break;
default:
return null;
}
if (IsMonoNestedCaptureScope(definition))
{
result.CaptureScope = null;
}
return result;
}
void HandleInitBlock(Block initBlock, int startIndex, DisplayClass result, ILVariable targetVariable)
{
if (initBlock == null)
return;
for (int i = startIndex; i < initBlock.Instructions.Count; i++)
{
var init = initBlock.Instructions[i];
if (!init.MatchStFld(out var target, out var field, out _))
break;
if (!target.MatchLdLocRef(targetVariable))
break;
if (result.VariablesToDeclare.ContainsKey((IField)field.MemberDefinition))
break;
var variable = AddVariable(result, (StObj)init, field);
result.VariablesToDeclare[(IField)field.MemberDefinition] = variable;
}
}
private Block FindDisplayStructInitBlock(ILVariable v)
{
var root = v.Function.Body;
return Visit(root)?.Ancestors.OfType<Block>().FirstOrDefault();
// Try to find a common ancestor of all uses of the variable v.
ILInstruction Visit(ILInstruction inst)
{
switch (inst)
{
case LdLoc l when l.Variable == v:
return l;
case StLoc s when s.Variable == v:
return s;
case LdLoca la when la.Variable == v:
return la;
default:
return VisitChildren(inst);
}
}
ILInstruction VisitChildren(ILInstruction inst)
{
// Visit all children of the instruction
ILInstruction result = null;
foreach (var child in inst.Children)
{
var newResult = Visit(child);
// As soon as there is a second use of v in this sub-tree,
// we can skip all other children and just return this node.
if (result == null)
{
result = newResult;
}
else if (newResult != null)
{
return inst;
}
}
// returns null, if v is not used in this sub-tree;
// returns a descendant of inst, if it is the only use of v in this sub-tree.
return result;
}
}
DisplayClass DetectDisplayClassInitializer(ILVariable v)
{
if (v.StoreInstructions.Count != 1 || !(v.StoreInstructions[0] is StLoc store && store.Parent is Block initializerBlock && initializerBlock.Kind == BlockKind.ObjectInitializer))
return null;
if (!(store.Value is NewObj newObj))
return null;
var definition = newObj.Method.DeclaringType.GetDefinition();
if (!ValidateDisplayClassDefinition(definition))
return null;
if (!ValidateConstructor(context, newObj.Method))
return null;
if (!initializerBlock.Parent.MatchStLoc(out var referenceVariable))
return null;
if (!referenceVariable.IsSingleDefinition)
return null;
if (!(referenceVariable.StoreInstructions.SingleOrDefault() is StLoc))
return null;
var result = new DisplayClass(referenceVariable, definition) {
CaptureScope = referenceVariable.CaptureScope,
Initializer = initializerBlock.Parent
};
HandleInitBlock(initializerBlock, 1, result, v);
return result;
}
private bool ValidateDisplayClassDefinition(ITypeDefinition definition)
{
if (definition == null)
return false;
if (definition.ParentModule.MetadataFile != context.PEFile)
return false;
// We do not want to accidentially transform state-machines and thus destroy them.
var token = (TypeDefinitionHandle)definition.MetadataToken;
var metadata = definition.ParentModule.MetadataFile.Metadata;
if (YieldReturnDecompiler.IsCompilerGeneratorEnumerator(token, metadata))
return false;
if (AsyncAwaitDecompiler.IsCompilerGeneratedStateMachine(token, metadata))
return false;
if (!context.Settings.AggressiveScalarReplacementOfAggregates)
{
if (definition.DeclaringTypeDefinition == null)
return false;
if (!IsPotentialClosure(context, definition))
return false;
}
return true;
}
internal static bool ValidateConstructor(ILTransformContext context, IMethod method)
{
try
{
if (method.Parameters.Count > 1)
return false;
var handle = (MethodDefinitionHandle)method.MetadataToken;
var module = (MetadataModule)method.ParentModule;
var file = module.MetadataFile;
if (handle.IsNil || file != context.PEFile)
return false;
var def = file.Metadata.GetMethodDefinition(handle);
if (def.RelativeVirtualAddress == 0)
return false;
var body = file.GetMethodBody(def.RelativeVirtualAddress);
// some compilers produce ctors with unused local variables
// see https://github.com/icsharpcode/ILSpy/issues/2174
//if (!body.LocalSignature.IsNil)
// return false;
if (body.ExceptionRegions.Length != 0)
return false;
var reader = body.GetILReader();
if (reader.Length < 7)
return false;
// IL_0000: ldarg.0
// IL_0001: call instance void [mscorlib]System.Object::.ctor()
// IL_0006: ret
if (!DecodeLdArg(ref reader, 0))
return false;
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Call)
return false;
var baseCtorHandle = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
if (baseCtorHandle.IsNil)
return false;
var objectCtor = module.ResolveMethod(baseCtorHandle, new TypeSystem.GenericContext());
if (!objectCtor.DeclaringType.IsKnownType(KnownTypeCode.Object))
return false;
if (!objectCtor.IsConstructor || objectCtor.Parameters.Count != 0)
return false;
if (method.Parameters.Count == 0)
return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret;
if (!object.Equals(method.Parameters[0].Type.GetDefinition(), method.DeclaringTypeDefinition))
return false;
if (!DecodeLdArg(ref reader, 1))
return false;
switch (DecodeOpCodeSkipNop(ref reader))
{
case ILOpCode.Brfalse:
reader.ReadInt32();
break;
case ILOpCode.Brfalse_s:
reader.ReadSByte();
break;
default:
return false;
}
bool copiedField = false;
while (true)
{
var opCode = DecodeOpCodeSkipNop(ref reader);
if (opCode == ILOpCode.Ret)
return copiedField;
if (!DecodeLdArg(opCode, ref reader, 0))
return false;
if (!DecodeLdArg(ref reader, 1))
return false;
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Ldfld)
return false;
var sourceField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Stfld)
return false;
var targetField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32());
if (sourceField.IsNil || sourceField != targetField)
return false;
copiedField = true;
}
}
catch (BadImageFormatException)
{
return false;
}
}
static bool DecodeLdArg(ref BlobReader reader, int expectedIndex)
{
return DecodeLdArg(DecodeOpCodeSkipNop(ref reader), ref reader, expectedIndex);
}
static bool DecodeLdArg(ILOpCode opCode, ref BlobReader reader, int expectedIndex)
{
switch (opCode)
{
case ILOpCode.Ldarg:
case ILOpCode.Ldarg_s:
return reader.DecodeIndex(opCode) == expectedIndex;
case ILOpCode.Ldarg_0:
return expectedIndex == 0;
case ILOpCode.Ldarg_1:
return expectedIndex == 1;
default:
return false;
}
}
static ILOpCode DecodeOpCodeSkipNop(ref BlobReader reader)
{
ILOpCode code;
do
{
code = reader.DecodeOpCode();
} while (code == ILOpCode.Nop);
return code;
}
VariableToDeclare AddVariable(DisplayClass result, StObj statement, IField field)
{
VariableToDeclare variable = new VariableToDeclare(result, field);
if (statement != null)
{
variable.Propagate(ResolveVariableToPropagate(statement.Value, field.Type));
variable.Initializers.Add(statement);
}
variable.UsesInitialValue =
result.Type.IsReferenceType != false || result.Variable.UsesInitialValue;
return variable;
}
/// <summary>
/// Resolves references to variables that can be propagated.
/// If a value does not match either LdLoc or a LdObj LdLdFlda* LdLoc chain, null is returned.
/// The if any of the variables/fields in the chain cannot be propagated, null is returned.
/// </summary>
ILVariable ResolveVariableToPropagate(ILInstruction value, IType expectedType = null)
{
ILVariable v;
switch (value)
{
case LdLoc load:
v = load.Variable;
if (v.Kind == VariableKind.Parameter)
{
if (v.LoadCount != 1 && !v.IsThis())
{
// If the variable is a parameter and it is used elsewhere, we cannot propagate it.
// "dc.field = v; dc.field.mutate(); use(v);" cannot turn to "v.mutate(); use(v)"
return null;
}
}
else
{
// Non-parameter propagation will later be checked, and will only be allowed for display classes
if (v.Type.IsReferenceType != true)
{
// don't allow propagation for display structs (as used with local functions)
return null;
}
}
if (!v.IsSingleDefinition)
{
// "dc.field = v; v = 42; use(dc.field)" cannot turn to "v = 42; use(v);"
return null;
}
if (!(expectedType == null || v.Kind == VariableKind.StackSlot || NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(v.Type, expectedType)))
return null;
return v;
case LdObj ldfld:
DisplayClass currentDisplayClass = null;
foreach (var item in ldfld.Target.Descendants)
{
if (IsDisplayClassLoad(item, out v))
{
if (!displayClasses.TryGetValue(v, out currentDisplayClass))
return null;
}
if (currentDisplayClass == null)
return null;
if (item is LdFlda ldf && currentDisplayClass.VariablesToDeclare.TryGetValue((IField)ldf.Field.MemberDefinition, out var vd))
{
if (!vd.CanPropagate)
return null;
if (!displayClasses.TryGetValue(vd.GetOrDeclare(), out currentDisplayClass))
return null;
}
}
return currentDisplayClass.Variable;
default:
return null;
}
}
private void Transform(ILFunction function)
{
VisitILFunction(function);
context.Step($"ResetHasInitialValueFlag", function);
foreach (var f in function.Descendants.OfType<ILFunction>())
{
RemoveDeadVariableInit.ResetUsesInitialValueFlag(f, context);
f.CapturedVariables.RemoveWhere(v => v.IsDead);
}
}
internal static bool IsClosure(ILTransformContext context, ILVariable variable, out ITypeDefinition closureType, out ILInstruction initializer)
{
closureType = null;
initializer = null;
if (variable.IsSingleDefinition && variable.StoreInstructions.SingleOrDefault() is StLoc inst)
{
initializer = inst;
if (IsClosureInit(context, inst, out closureType))
{
return true;
}
}
closureType = variable.Type.GetDefinition();
if (context.Settings.LocalFunctions && closureType?.Kind == TypeKind.Struct
&& variable.UsesInitialValue && IsPotentialClosure(context, closureType))
{
initializer = Block.GetContainingStatement(variable.AddressInstructions.OrderBy(i => i.StartILOffset).First());
return true;
}
return false;
}
static bool IsClosureInit(ILTransformContext context, StLoc inst, out ITypeDefinition closureType)
{
if (inst.Value is NewObj newObj)
{
closureType = newObj.Method.DeclaringTypeDefinition;
return closureType != null && IsPotentialClosure(context, newObj);
}
closureType = null;
return false;
}
bool IsMonoNestedCaptureScope(ITypeDefinition closureType)
{
if (!closureType.Name.Contains("AnonStorey"))
return false;
var decompilationContext = new SimpleTypeResolveContext(context.Function.Method);
return closureType.Fields.Any(f => IsPotentialClosure(decompilationContext.CurrentTypeDefinition, f.ReturnType.GetDefinition()));
}
/// <summary>
/// mcs likes to optimize closures in yield state machines away by moving the captured variables' fields into the state machine type,
/// We construct a <see cref="DisplayClass"/> that spans the whole method body.
/// </summary>
DisplayClass HandleMonoStateMachine(ILFunction function, ILVariable thisVariable)
{
if (!(function.StateMachineCompiledWithMono && thisVariable.IsThis()))
return null;
// Special case for Mono-compiled yield state machines
ITypeDefinition closureType = thisVariable.Type.GetDefinition();
if (!(closureType != decompilationContext.CurrentTypeDefinition
&& IsPotentialClosure(decompilationContext.CurrentTypeDefinition, closureType, allowTypeImplementingInterfaces: true)))
return null;
var displayClass = new DisplayClass(thisVariable, thisVariable.Type.GetDefinition());
displayClass.CaptureScope = (BlockContainer)function.Body;
foreach (var stateMachineVariable in function.Variables)
{
if (stateMachineVariable.StateMachineField == null || displayClass.VariablesToDeclare.ContainsKey(stateMachineVariable.StateMachineField))
continue;
VariableToDeclare variableToDeclare = new VariableToDeclare(displayClass, stateMachineVariable.StateMachineField, stateMachineVariable);
displayClass.VariablesToDeclare.Add(stateMachineVariable.StateMachineField, variableToDeclare);
}
if (!function.Method.IsStatic && FindThisField(out var thisField))
{
var thisVar = function.Variables
.FirstOrDefault(t => t.IsThis() && t.Type.GetDefinition() == decompilationContext.CurrentTypeDefinition);
if (thisVar == null)
{
thisVar = new ILVariable(VariableKind.Parameter, decompilationContext.CurrentTypeDefinition, -1) {
Name = "this", StateMachineField = thisField
};
function.Variables.Add(thisVar);
}
else if (thisVar.StateMachineField != null && displayClass.VariablesToDeclare.ContainsKey(thisVar.StateMachineField))
{
// "this" was already added previously, no need to add it twice.
return displayClass;
}
VariableToDeclare variableToDeclare = new VariableToDeclare(displayClass, thisField, thisVar);
displayClass.VariablesToDeclare.Add(thisField, variableToDeclare);
}
return displayClass;
bool FindThisField(out IField foundField)
{
foundField = null;
foreach (var field in closureType.GetFields(f2 => !f2.IsStatic && !displayClass.VariablesToDeclare.ContainsKey(f2) && f2.Type.GetDefinition() == decompilationContext.CurrentTypeDefinition))
{
thisField = field;
return true;
}
return false;
}
}
internal static bool IsPotentialClosure(ILTransformContext context, NewObj inst)
{
var decompilationContext = new SimpleTypeResolveContext(context.Function.Ancestors.OfType<ILFunction>().Last().Method);
return IsPotentialClosure(decompilationContext.CurrentTypeDefinition, inst.Method.DeclaringTypeDefinition);
}
internal static bool IsPotentialClosure(ILTransformContext context, ITypeDefinition potentialDisplayClass)
{
var decompilationContext = new SimpleTypeResolveContext(context.Function.Ancestors.OfType<ILFunction>().Last().Method);
return IsPotentialClosure(decompilationContext.CurrentTypeDefinition, potentialDisplayClass);
}
internal static bool IsPotentialClosure(ITypeDefinition decompiledTypeDefinition, ITypeDefinition potentialDisplayClass, bool allowTypeImplementingInterfaces = false)
{
if (potentialDisplayClass == null || !potentialDisplayClass.IsCompilerGeneratedOrIsInCompilerGeneratedClass())
return false;
switch (potentialDisplayClass.Kind)
{
case TypeKind.Struct:
break;
case TypeKind.Class:
if (!allowTypeImplementingInterfaces)
{
if (!potentialDisplayClass.DirectBaseTypes.All(t => t.IsKnownType(KnownTypeCode.Object)))
return false;
}
break;
default:
return false;
}
// Make sure that potentialDisplayCLass and decompiledTypeDefinition are part of the same type tree
// Either decompiledTypeDefinition is an ancestor type of potentialDisplayClass or both have
// at least one common ancestor.
var potentialDisplayClassAncestors = new HashSet<ITypeDefinition>();
var potentialDisplayClassParent = potentialDisplayClass.DeclaringTypeDefinition;
while (potentialDisplayClassParent != null)
{
potentialDisplayClassAncestors.Add(potentialDisplayClassParent);
potentialDisplayClassParent = potentialDisplayClassParent.DeclaringTypeDefinition;
}
var decompiledTypeDefinitionOrAncestor = decompiledTypeDefinition;
while (decompiledTypeDefinitionOrAncestor != null)
{
if (potentialDisplayClassAncestors.Contains(decompiledTypeDefinitionOrAncestor))
return true;
decompiledTypeDefinitionOrAncestor = decompiledTypeDefinitionOrAncestor.DeclaringTypeDefinition;
}
return false;
}
readonly Stack<ILFunction> currentFunctions = new Stack<ILFunction>();
protected internal override void VisitILFunction(ILFunction function)
{
context.StepStartGroup("Visit " + function.Name);
try
{
this.currentFunctions.Push(function);
base.VisitILFunction(function);
}
finally
{
this.currentFunctions.Pop();
context.StepEndGroup(keepIfEmpty: true);
}
}
protected override void Default(ILInstruction inst)
{
ILInstruction next;
for (var child = inst.Children.FirstOrDefault(); child != null; child = next)
{
next = child.GetNextSibling();
child.AcceptVisitor(this);
}
}
protected internal override void VisitStLoc(StLoc inst)
{
DisplayClass displayClass;
if (inst.Parent is Block parentBlock && inst.Variable.IsSingleDefinition)
{
if ((inst.Variable.Kind == VariableKind.Local || inst.Variable.Kind == VariableKind.StackSlot) && inst.Variable.LoadCount == 0)
{
// traverse pre-order, so that we do not have to deal with more special cases afterwards
base.VisitStLoc(inst);
if (inst.Value is StLoc || inst.Value is CompoundAssignmentInstruction)
{
context.Step($"Remove unused variable assignment {inst.Variable.Name}", inst);
var replacement = inst.Value;
inst.ReplaceWith(replacement);
context.EndStep(replacement);
}
return;
}
if (displayClasses.TryGetValue(inst.Variable, out displayClass) && displayClass.Initializer == inst)
{
// inline contents of object initializer block
if (inst.Value is Block initBlock && initBlock.Kind == BlockKind.ObjectInitializer)
{
context.Step($"Remove initializer of {inst.Variable.Name}", inst);
ILInstruction firstInlinedStore = null;
// Stores are appended after the initializer, in source order; a store that
// is dropped must not leave a gap, so the position is tracked separately
// from the loop index.
int insertionIndex = inst.ChildIndex;
for (int i = 1; i < initBlock.Instructions.Count; i++)
{
var stobj = (StObj)initBlock.Instructions[i];
var variable = displayClass.VariablesToDeclare[(IField)((LdFlda)stobj.Target).Field.MemberDefinition];
// A propagated field is replaced by the variable it was initialized from,
// so keeping its initializer would assign that variable to itself - and
// where the variable is 'this', the result is not even valid C#. Stores
// outside an object-initializer block are dropped in VisitStObj under the
// same condition.
if (variable.CanPropagate && variable.Initializers.Contains(stobj))
continue;
var inlinedStore = new StLoc(variable.GetOrDeclare(), stobj.Value).WithILRange(stobj);
parentBlock.Instructions.Insert(++insertionIndex, inlinedStore);
firstInlinedStore ??= inlinedStore;
}
context.EndStep(firstInlinedStore);
}
context.Step($"Remove initializer of {inst.Variable.Name}", inst);
parentBlock.Instructions.Remove(inst);
return;
}
if (inst.Value is LdLoc || inst.Value is LdObj)
{
// in some cases (e.g. if inlining fails), there can be a reference to a display class in a stack slot,
// in that case it is necessary to resolve the reference and iff it can be propagated, replace all loads
// of the single-definition.
if (!displayClassCopyMap.TryGetValue(inst.Variable, out var referencedDisplayClass))
{
referencedDisplayClass = ResolveVariableToPropagate(inst.Value);
}
if (referencedDisplayClass != null && displayClasses.TryGetValue(referencedDisplayClass, out _))
{
context.Step($"Propagate reference to {referencedDisplayClass.Name} in {inst.Variable}", inst);
ILInstruction firstReplacement = null;
foreach (var ld in inst.Variable.LoadInstructions.ToArray())
{
var newLoad = new LdLoc(referencedDisplayClass).WithILRange(ld);
ld.ReplaceWith(newLoad);
firstReplacement ??= newLoad;
}
context.EndStep(firstReplacement);
parentBlock.Instructions.Remove(inst);
return;
}
}
}
base.VisitStLoc(inst);
}
protected internal override void VisitStObj(StObj inst)
{
if (IsDisplayClassFieldAccess(inst.Target, out var v, out var displayClass, out var field))
{
VariableToDeclare vd = displayClass.VariablesToDeclare[(IField)field.MemberDefinition];
if (vd.CanPropagate && vd.Initializers.Contains(inst))
{
if (inst.Parent is Block containingBlock)
{
context.Step($"Remove initializer of {v.Name}.{vd.Name} due to propagation", inst);
containingBlock.Instructions.Remove(inst);
return;
}
}
if (inst.Value is LdLoc ldLoc && ldLoc.Variable is { IsSingleDefinition: true, CaptureScope: null }
&& ldLoc.Variable.StoreInstructions.FirstOrDefault() is StLoc stloc
&& stloc.Parent is Block block)
{
ILInlining.InlineOneIfPossible(block, stloc.ChildIndex, InliningOptions.None, context);
}
}
base.VisitStObj(inst);
EarlyExpressionTransforms.StObjToStLoc(inst, context);
}
protected internal override void VisitLdObj(LdObj inst)
{
base.VisitLdObj(inst);
EarlyExpressionTransforms.LdObjToLdLoc(inst, context);
}
private bool IsDisplayClassLoad(ILInstruction target, out ILVariable variable)
{
// We cannot use MatchLdLocRef here because local functions use ref parameters
if (!target.MatchLdLoc(out variable) && !target.MatchLdLoca(out variable))
return false;
if (displayClassCopyMap.TryGetValue(variable, out ILVariable other))
variable = other;
return true;
}
private bool IsDisplayClassFieldAccess(ILInstruction inst,
out ILVariable displayClassVar, out DisplayClass displayClass, out IField field)
{
displayClass = null;
displayClassVar = null;
field = null;
if (inst is not LdFlda ldflda)
return false;
field = ldflda.Field;
return IsDisplayClassLoad(ldflda.Target, out displayClassVar)
&& displayClasses.TryGetValue(displayClassVar, out displayClass);
}
protected internal override void VisitLdFlda(LdFlda inst)
{
base.VisitLdFlda(inst);
// Get display class info
if (!IsDisplayClassFieldAccess(inst, out _, out DisplayClass displayClass, out IField field))
return;
var keyField = (IField)field.MemberDefinition;
var v = displayClass.VariablesToDeclare[keyField];
context.Step($"Replace {field.Name} with captured variable {v.Name}", inst);
ILVariable variable = v.GetOrDeclare();
var replacement = new LdLoca(variable).WithILRange(inst);
inst.ReplaceWith(replacement);
context.EndStep(replacement);
// add captured variable to all descendant functions from the declaring function to this use-site function
foreach (var f in currentFunctions)
{
if (f == variable.Function)
break;
f.CapturedVariables.Add(variable);
}
}
}
}