// 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)
		{
			Debug.Assert(v == displayClass.Variable);
			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);
						}
						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 true;
					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(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(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.PEFile != 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.PEFile.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;
		}

		private bool ValidateConstructor(IMethod method)
		{
			try
			{
				if (method.Parameters.Count != 0)
					return false;
				var handle = (MethodDefinitionHandle)method.MetadataToken;
				var module = (MetadataModule)method.ParentModule;
				var file = module.PEFile;
				if (handle.IsNil || file != context.PEFile)
					return false;
				var def = file.Metadata.GetMethodDefinition(handle);
				if (def.RelativeVirtualAddress == 0)
					return false;
				var body = file.Reader.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
				var opCode = DecodeOpCodeSkipNop(ref reader);
				switch (opCode)
				{
					case ILOpCode.Ldarg:
					case ILOpCode.Ldarg_s:
						if (reader.DecodeIndex(opCode) != 0)
							return false;
						break;
					case ILOpCode.Ldarg_0:
						// OK
						break;
					default:
						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;
				return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret;
			}
			catch (BadImageFormatException)
			{
				return false;
			}
		}

		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 || v.Type.Equals(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 = LocalFunctionDecompiler.GetStatement(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;
			}

			while (potentialDisplayClass != decompiledTypeDefinition)
			{
				potentialDisplayClass = potentialDisplayClass.DeclaringTypeDefinition;
				if (potentialDisplayClass == null)
					return false;
			}
			return true;
		}

		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);
						inst.ReplaceWith(inst.Value);
					}
					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);
						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];
							parentBlock.Instructions.Insert(inst.ChildIndex + i, new StLoc(variable.GetOrDeclare(), stobj.Value).WithILRange(stobj));
						}
					}
					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);
						foreach (var ld in inst.Variable.LoadInstructions.ToArray())
						{
							ld.ReplaceWith(new LdLoc(referencedDisplayClass).WithILRange(ld));
						}
						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();
			inst.ReplaceWith(new LdLoca(variable).WithILRange(inst));
			// 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);
			}
		}
	}
}