// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
// 
// 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 ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ICSharpCode.Decompiler.IL.ControlFlow
{
	class YieldReturnDecompiler : IILTransform
	{
		// For a description on the code generated by the C# compiler for yield return:
		// http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx
		// The idea here is:
		// - Figure out whether the current method is instanciating an enumerator
		// - Figure out which of the fields is the state field
		// - Construct an exception table based on states. This allows us to determine, for each state, what the parent try block is.
		// See http://community.sharpdevelop.net/blogs/danielgrunwald/archive/2011/03/06/ilspy-yield-return.aspx
		// for a description of this step.
		ILTransformContext context;
		/// The type that contains the function being decompiled.
		TypeDefinition currentType;
		/// The compiler-generated enumerator class.
		/// Set in MatchEnumeratorCreationPattern()
		TypeDefinition enumeratorType;
		/// The constructor of the compiler-generated enumerator class.
		/// Set in MatchEnumeratorCreationPattern()
		MethodDefinition enumeratorCtor;
		/// Set in MatchEnumeratorCreationPattern()
		bool isCompiledWithMono;
		/// The dispose method of the compiler-generated enumerator class.
		/// Set in ConstructExceptionTable()
		MethodDefinition disposeMethod;
		/// The field in the compiler-generated class holding the current state of the state machine
		/// Set in AnalyzeCtor() for MS, MatchEnumeratorCreationPattern() or AnalyzeMoveNext() for Mono
		IField stateField;
		/// The backing field of the 'Current' property in the compiler-generated class
		/// Set in AnalyzeCurrentProperty()
		IField currentField;
		/// Maps the fields of the compiler-generated class to the original parameters.
		/// Set in MatchEnumeratorCreationPattern() and ResolveIEnumerableIEnumeratorFieldMapping()
		readonly Dictionary fieldToParameterMap = new Dictionary();
		/// This dictionary stores the information extracted from the Dispose() method:
		/// for each "Finally Method", it stores the set of states for which the method is being called.
		/// Set in ConstructExceptionTable()
		Dictionary finallyMethodToStateRange;
		/// 
		/// For each finally method, stores the target state when entering the finally block,
		/// and the decompiled code of the finally method body.
		/// 
		readonly Dictionary decompiledFinallyMethods = new Dictionary();
		
		/// 
		/// Temporary stores for 'yield break'.
		/// 
		readonly List returnStores = new List();
		/// 
		/// Local bool variable in MoveNext() that signifies whether to skip finally bodies.
		/// 
		ILVariable skipFinallyBodies;
		/// 
		/// Set of variables might hold copies of the generated state field.
		/// 
		HashSet cachedStateVars;
		#region Run() method
		public void Run(ILFunction function, ILTransformContext context)
		{
			if (!context.Settings.YieldReturn)
				return; // abort if enumerator decompilation is disabled
			this.context = context;
			this.currentType = function.CecilMethod.DeclaringType;
			this.enumeratorType = null;
			this.enumeratorCtor = null;
			this.stateField = null;
			this.currentField = null;
			this.fieldToParameterMap.Clear();
			this.finallyMethodToStateRange = null;
			this.decompiledFinallyMethods.Clear();
			this.returnStores.Clear();
			this.skipFinallyBodies = null;
			this.cachedStateVars = null;
			if (!MatchEnumeratorCreationPattern(function))
				return;
			BlockContainer newBody;
			try {
				AnalyzeCtor();
				AnalyzeCurrentProperty();
				ResolveIEnumerableIEnumeratorFieldMapping();
				ConstructExceptionTable();
				newBody = AnalyzeMoveNext();
			} catch (SymbolicAnalysisFailedException) {
				return;
			}
			context.Step("Replacing body with MoveNext() body", function);
			function.IsIterator = true;
			function.StateMachineCompiledWithMono = true;
			function.Body = newBody;
			// register any locals used in newBody
			function.Variables.AddRange(newBody.Descendants.OfType().Select(inst => inst.Variable).Distinct());
			function.CheckInvariant(ILPhase.Normal);
			PrintFinallyMethodStateRanges(newBody);
			// Add state machine field meta-data to parameter ILVariables.
			foreach (var (f, p) in fieldToParameterMap) {
				p.StateMachineField = f;
			}
			context.Step("Delete unreachable blocks", function);
			// Note: because this only deletes blocks outright, the 'stateChanges' entries remain valid
			// (though some may point to now-deleted blocks)
			newBody.SortBlocks(deleteUnreachableBlocks: true);
			if (isCompiledWithMono) {
				// mono has try-finally inline (like async on MS); we also need to sort nested blocks:
				foreach (var nestedContainer in newBody.Blocks.SelectMany(c => c.Descendants).OfType()) {
					nestedContainer.SortBlocks(deleteUnreachableBlocks: true);
				}
			} else {
				DecompileFinallyBlocks();
				ReconstructTryFinallyBlocks(function);
			}
			context.Step("Translate fields to local accesses", function);
			TranslateFieldsToLocalAccess(function, function, fieldToParameterMap);
			CleanSkipFinallyBodies(function);
			// On mono, we still need to remove traces of the state variable(s):
			if (isCompiledWithMono) {
				if (fieldToParameterMap.TryGetValue(stateField, out var stateVar)) {
					returnStores.AddRange(stateVar.StoreInstructions.OfType());
				}
				foreach (var cachedStateVar in cachedStateVars) {
					returnStores.AddRange(cachedStateVar.StoreInstructions.OfType());
				}
			}
			if (returnStores.Count > 0) {
				context.Step("Remove temporaries", function);
				foreach (var store in returnStores) {
					if (store.Variable.LoadCount == 0 && store.Variable.AddressCount == 0 && store.Parent is Block block) {
						Debug.Assert(SemanticHelper.IsPure(store.Value.Flags));
						block.Instructions.Remove(store);
					}
				}
			}
			// Re-run control flow simplification over the newly constructed set of gotos,
			// and inlining because TranslateFieldsToLocalAccess() might have opened up new inlining opportunities.
			function.RunTransforms(CSharpDecompiler.EarlyILTransforms(), context);
		}
		#endregion
		#region Match the enumerator creation pattern
		bool MatchEnumeratorCreationPattern(ILFunction function)
		{
			Block body = SingleBlock(function.Body);
			if (body == null || body.Instructions.Count == 0) {
				return false;
			}
			ILInstruction newObj;
			if (body.Instructions.Count == 1) {
				// No parameters passed to enumerator (not even 'this'):
				// ret(newobj(...))
				if (!body.Instructions[0].MatchReturn(out newObj))
					return false;
				if (MatchEnumeratorCreationNewObj(newObj)) {
					return true;
				} else if (MatchMonoEnumeratorCreationNewObj(newObj)) {
					isCompiledWithMono = true;
					return true;
				} else {
					return false;
				}
			}
			// If there's parameters passed to the helper class, the class instance is first
			// stored in a variable, then the parameters are copied over, then the instance is returned.
			int pos = 0;
			// stloc(var_1, newobj(..))
			if (!body.Instructions[pos].MatchStLoc(out var var1, out newObj))
				return false;
			if (MatchEnumeratorCreationNewObj(newObj)) {
				pos++; // OK
				isCompiledWithMono = false;
			} else if (MatchMonoEnumeratorCreationNewObj(newObj)) {
				pos++;
				isCompiledWithMono = true;
			} else {
				return false;
			}
			
			for (; pos < body.Instructions.Count; pos++) {
				// stfld(..., ldloc(var_1), ldloc(parameter))
				// or (in structs): stfld(..., ldloc(var_1), ldobj(ldloc(this)))
				if (!body.Instructions[pos].MatchStFld(out var ldloc, out var storedField, out var value))
					break;
				if (!ldloc.MatchLdLoc(var1)) {
					return false;
				}
				if (value.MatchLdLoc(out var parameter) && parameter.Kind == VariableKind.Parameter) {
					fieldToParameterMap[(IField)storedField.MemberDefinition] = parameter;
				} else if (value is LdObj ldobj && ldobj.Target.MatchLdThis()) {
					// copy of 'this' in struct
					fieldToParameterMap[(IField)storedField.MemberDefinition] = ((LdLoc)ldobj.Target).Variable;
				} else {
					return false;
				}
			}
			// In debug builds, the compiler may copy the var1 into another variable (var2) before returning it.
			if (body.Instructions[pos].MatchStLoc(out var var2, out var ldlocForStloc2)
				&& ldlocForStloc2.MatchLdLoc(var1)) {
				// stloc(var_2, ldloc(var_1))
				pos++;
			}
			if (isCompiledWithMono) {
				// Mono initializes the state field separately:
				// (but not if it's left at the default value 0)
				if (body.Instructions[pos].MatchStFld(out var target, out var field, out var value)
					&& target.MatchLdLoc(var2 ?? var1)
					&& (value.MatchLdcI4(-2) || value.MatchLdcI4(0)))
				{
					stateField = (IField)field.MemberDefinition;
					isCompiledWithMono = true;
					pos++;
				}
			}
			if (body.Instructions[pos].MatchReturn(out var retVal)
				&& retVal.MatchLdLoc(var2 ?? var1)) {
				// ret(ldloc(var_2))
				return true;
			} else {
				return false;
			}
		}
		/// 
		/// Matches the body of a method as a single basic block.
		/// 
		internal static Block SingleBlock(ILInstruction body)
		{
			var block = body as Block;
			if (body is BlockContainer blockContainer && blockContainer.Blocks.Count == 1) {
				block = blockContainer.Blocks.Single() as Block;
			}
			return block;
		}
		/// 
		/// Matches the newobj instruction that creates an instance of the compiler-generated enumerator helper class.
		/// 
		bool MatchEnumeratorCreationNewObj(ILInstruction inst)
		{
			// newobj(CurrentType/...::.ctor, ldc.i4(-2))
			if (!(inst is NewObj newObj))
				return false;
			if (newObj.Arguments.Count != 1)
				return false;
			if (!newObj.Arguments[0].MatchLdcI4(out int initialState))
				return false;
			if (!(initialState == -2 || initialState == 0))
				return false;
			enumeratorCtor = context.TypeSystem.GetCecil(newObj.Method) as MethodDefinition;
			enumeratorType = enumeratorCtor?.DeclaringType;
			return enumeratorType?.DeclaringType == currentType
				&& IsCompilerGeneratorEnumerator(enumeratorType);
		}
		bool MatchMonoEnumeratorCreationNewObj(ILInstruction inst)
		{
			// mcs generates iterators that take no parameters in the ctor
			if (!(inst is NewObj newObj))
				return false;
			if (newObj.Arguments.Count != 0)
				return false;
			enumeratorCtor = context.TypeSystem.GetCecil(newObj.Method) as MethodDefinition;
			enumeratorType = enumeratorCtor?.DeclaringType;
			return enumeratorType?.DeclaringType == currentType
				&& IsCompilerGeneratorEnumerator(enumeratorType);
		}
		public static bool IsCompilerGeneratorEnumerator(TypeDefinition type)
		{
			if (!(type?.DeclaringType != null && type.IsCompilerGenerated()))
				return false;
			foreach (var i in type.Interfaces) {
				var tr = i.InterfaceType;
				if (tr.Namespace == "System.Collections" && tr.Name == "IEnumerator")
					return true;
			}
			return false;
		}
		#endregion
		#region Figure out what the 'state' field is (analysis of .ctor())
		/// 
		/// Looks at the enumerator's ctor and figures out which of the fields holds the state.
		/// 
		void AnalyzeCtor()
		{
			Block body = SingleBlock(CreateILAst(enumeratorCtor, context).Body);
			if (body == null)
				throw new SymbolicAnalysisFailedException("Missing enumeratorCtor.Body");
			foreach (var inst in body.Instructions) {
				if (inst.MatchStFld(out var target, out var field, out var value)
					&& target.MatchLdThis()
					&& value.MatchLdLoc(out var arg)
					&& arg.Kind == VariableKind.Parameter && arg.Index == 0) {
					stateField = (IField)field.MemberDefinition;
				}
			}
			if (stateField == null && !isCompiledWithMono)
				throw new SymbolicAnalysisFailedException("Could not find stateField");
		}
		/// 
		/// Creates ILAst for the specified method, optimized up to before the 'YieldReturn' step.
		/// 
		internal static ILFunction CreateILAst(MethodDefinition method, ILTransformContext context)
		{
			if (method == null || !method.HasBody)
				throw new SymbolicAnalysisFailedException();
			var typeSystem = context.TypeSystem;
			var sdtp = typeSystem as SpecializingDecompilerTypeSystem;
			if (sdtp != null) {
				typeSystem = new SpecializingDecompilerTypeSystem(
					sdtp.Context,
					new TypeParameterSubstitution(
						(sdtp.Substitution.ClassTypeArguments ?? EmptyList.Instance)
						.Concat(sdtp.Substitution.MethodTypeArguments ?? EmptyList.Instance).ToArray(),
						EmptyList.Instance
					)
				);
			}
			var il = new ILReader(typeSystem).ReadIL(method.Body, context.CancellationToken);
			il.RunTransforms(CSharpDecompiler.EarlyILTransforms(true),
				new ILTransformContext(il, typeSystem, context.Settings) {
					CancellationToken = context.CancellationToken,
					DecompileRun = context.DecompileRun
				});
			return il;
		}
		#endregion
		#region Figure out what the 'current' field is (analysis of get_Current())
		/// 
		/// Looks at the enumerator's get_Current method and figures out which of the fields holds the current value.
		/// 
		void AnalyzeCurrentProperty()
		{
			MethodDefinition getCurrentMethod = enumeratorType.Methods.FirstOrDefault(
				m => m.Name.StartsWith("System.Collections.Generic.IEnumerator", StringComparison.Ordinal)
				&& m.Name.EndsWith(".get_Current", StringComparison.Ordinal));
			Block body = SingleBlock(CreateILAst(getCurrentMethod, context).Body);
			if (body == null)
				throw new SymbolicAnalysisFailedException();
			if (body.Instructions.Count == 1) {
				// release builds directly return the current field
				// ret(ldfld F(ldloc(this)))
				if (body.Instructions[0].MatchReturn(out var retVal)
					&& retVal.MatchLdFld(out var target, out var field)
					&& target.MatchLdThis()) {
					currentField = (IField)field.MemberDefinition;
				}
			} else if (body.Instructions.Count == 2) {
				// debug builds store the return value in a temporary
				// stloc V = ldfld F(ldloc(this))
				// ret(ldloc V)
				if (body.Instructions[0].MatchStLoc(out var v, out var ldfld)
					&& ldfld.MatchLdFld(out var target, out var field)
					&& target.MatchLdThis()
					&& body.Instructions[1].MatchReturn(out var retVal)
					&& retVal.MatchLdLoc(v)) {
					currentField = (IField)field.MemberDefinition;
				}
			}
			if (currentField == null)
				throw new SymbolicAnalysisFailedException("Could not find currentField");
		}
		#endregion
		#region Figure out the mapping of IEnumerable fields to IEnumerator fields  (analysis of GetEnumerator())
		void ResolveIEnumerableIEnumeratorFieldMapping()
		{
			MethodDefinition getEnumeratorMethod = enumeratorType.Methods.FirstOrDefault(
				m => m.Name.StartsWith("System.Collections.Generic.IEnumerable", StringComparison.Ordinal)
				&& m.Name.EndsWith(".GetEnumerator", StringComparison.Ordinal));
			if (getEnumeratorMethod == null)
				return; // no mappings (maybe it's just an IEnumerator implementation?)
			var function = CreateILAst(getEnumeratorMethod, context);
			foreach (var block in function.Descendants.OfType()) {
				foreach (var inst in block.Instructions) {
					// storeTarget.storeField = this.loadField;
					if (inst.MatchStFld(out var storeTarget, out var storeField, out var storeValue)
						&& storeValue.MatchLdFld(out var loadTarget, out var loadField)
						&& loadTarget.MatchLdThis()) {
						storeField = (IField)storeField.MemberDefinition;
						loadField = (IField)loadField.MemberDefinition;
						if (fieldToParameterMap.TryGetValue(loadField, out var mappedParameter))
							fieldToParameterMap[storeField] = mappedParameter;
					}
				}
			}
		}
		#endregion
		#region Construction of the exception table (analysis of Dispose())
		// We construct the exception table by analyzing the enumerator's Dispose() method.
		void ConstructExceptionTable()
		{
			if (isCompiledWithMono) {
				// On mono, we don't need to analyse Dispose() to reconstruct the try-finally structure.
				disposeMethod = null;
				finallyMethodToStateRange = null;
				return;
			}
			disposeMethod = enumeratorType.Methods.FirstOrDefault(m => m.Name == "System.IDisposable.Dispose");
			var function = CreateILAst(disposeMethod, context);
			var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorDispose, stateField);
			rangeAnalysis.AssignStateRanges(function.Body, LongSet.Universe);
			finallyMethodToStateRange = rangeAnalysis.finallyMethodToStateRange;
		}
		
		[Conditional("DEBUG")]
		void PrintFinallyMethodStateRanges(BlockContainer bc)
		{
			if (finallyMethodToStateRange == null)
				return;
			foreach (var (method, stateRange) in finallyMethodToStateRange) {
				bc.Blocks[0].Instructions.Insert(0, new Nop {
					Comment = method.Name + " in " + stateRange
				});
			}
		}
		#endregion
		#region Analyze MoveNext() and generate new body
		BlockContainer AnalyzeMoveNext()
		{
			context.StepStartGroup("AnalyzeMoveNext");
			MethodDefinition moveNextMethod = enumeratorType.Methods.FirstOrDefault(m => m.Name == "MoveNext");
			ILFunction moveNextFunction = CreateILAst(moveNextMethod, context);
			// Copy-propagate temporaries holding a copy of 'this'.
			// This is necessary because the old (pre-Roslyn) C# compiler likes to store 'this' in temporary variables.
			foreach (var stloc in moveNextFunction.Descendants.OfType().Where(s => s.Variable.IsSingleDefinition && s.Value.MatchLdThis()).ToList()) {
				CopyPropagation.Propagate(stloc, context);
			}
			var body = (BlockContainer)moveNextFunction.Body;
			if (body.Blocks.Count == 1 && body.Blocks[0].Instructions.Count == 1 && body.Blocks[0].Instructions[0] is TryFault tryFault) {
				body = (BlockContainer)tryFault.TryBlock;
				var faultBlockContainer = tryFault.FaultBlock as BlockContainer;
				if (faultBlockContainer?.Blocks.Count != 1)
					throw new SymbolicAnalysisFailedException("Unexpected number of blocks in MoveNext() fault block");
				var faultBlock = faultBlockContainer.Blocks.Single();
				if (!(faultBlock.Instructions.Count == 2
					&& faultBlock.Instructions[0] is Call call
					&& context.TypeSystem.GetCecil(call.Method) == disposeMethod
					&& call.Arguments.Count == 1
					&& call.Arguments[0].MatchLdThis()
					&& faultBlock.Instructions[1].MatchLeave(faultBlockContainer))) {
					throw new SymbolicAnalysisFailedException("Unexpected fault block contents in MoveNext()");
				}
			}
			if (stateField == null) {
				// With mono-compiled state machines, it's possible that we haven't discovered the state field
				// yet because the compiler let it be implicitly initialized to 0.
				// In this case, we must discover it from the first instruction in MoveNext():
				if (body.EntryPoint.Instructions[0] is StLoc stloc
					&& stloc.Value.MatchLdFld(out var target, out var field)
					&& target.MatchLdThis() && field.Type.IsKnownType(KnownTypeCode.Int32)) {
					stateField = (IField)field.MemberDefinition;
				} else {
					throw new SymbolicAnalysisFailedException("Could not find state field.");
				}
			}
			skipFinallyBodies = null;
			if (isCompiledWithMono) {
				// Mono uses skipFinallyBodies; find out which variable that is:
				foreach (var tryFinally in body.Descendants.OfType()) {
					if ((tryFinally.FinallyBlock as BlockContainer)?.EntryPoint.Instructions[0] is IfInstruction ifInst) {
						if (ifInst.Condition.MatchLogicNot(out var arg) && arg.MatchLdLoc(out var v) && v.Type.IsKnownType(KnownTypeCode.Boolean)) {
							bool isInitializedInEntryBlock = false;
							for (int i = 0; i < 3; i++) {
								if (body.EntryPoint.Instructions.ElementAtOrDefault(i) is StLoc stloc
									&& stloc.Variable == v && stloc.Value.MatchLdcI4(0))
								{
									isInitializedInEntryBlock = true;
									break;
								}
							}
							if (isInitializedInEntryBlock) {
								skipFinallyBodies = v;
								break;
							}
						}
					}
				}
			}
			PropagateCopiesOfFields(body);
			// Note: body may contain try-catch or try-finally statements that have nested block containers,
			// but those cannot contain any yield statements.
			// So for reconstructing the control flow, we only consider the blocks directly within body.
			var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorMoveNext, stateField);
			rangeAnalysis.skipFinallyBodies = skipFinallyBodies;
			rangeAnalysis.CancellationToken = context.CancellationToken;
			rangeAnalysis.AssignStateRanges(body, LongSet.Universe);
			cachedStateVars = rangeAnalysis.CachedStateVars.ToHashSet();
			var newBody = ConvertBody(body, rangeAnalysis);
			moveNextFunction.Variables.Clear();
			// release references from old moveNextFunction to instructions that were moved over to newBody
			moveNextFunction.ReleaseRef();
			context.StepEndGroup();
			return newBody;
		}
		private void PropagateCopiesOfFields(BlockContainer body)
		{
			// Roslyn may optimize MoveNext() by copying fields from the iterator class into local variables
			// at the beginning of MoveNext(). Undo this optimization.
			context.StepStartGroup("PropagateCopiesOfFields");
			var mutableFields = body.Descendants.OfType().Where(ldflda => ldflda.Parent.OpCode != OpCode.LdObj).Select(ldflda => ldflda.Field).ToHashSet();
			for (int i = 0; i < body.EntryPoint.Instructions.Count; i++) {
				if (body.EntryPoint.Instructions[i] is StLoc store
					&& store.Variable.IsSingleDefinition
					&& store.Value is LdObj ldobj
					&& ldobj.Target is LdFlda ldflda
					&& ldflda.Target.MatchLdThis())
				{
					if (!mutableFields.Contains(ldflda.Field)) {
						// perform copy propagation: (unlike CopyPropagation.Propagate(), copy the ldobj arguments as well)
						foreach (var expr in store.Variable.LoadInstructions.ToArray()) {
							expr.ReplaceWith(store.Value.Clone());
						}
						body.EntryPoint.Instructions.RemoveAt(i--);
					} else if (ldflda.Field.MemberDefinition == stateField.MemberDefinition) {
						continue;
					} else {
						break; // unsupported: load of mutable field (other than state field)
					}
				} else {
					break; // unknown instruction
				}
			}
			context.StepEndGroup();
		}
		/// 
		/// Convert the old body (of MoveNext function) to the new body (of decompiled iterator method).
		/// 
		/// * Replace the sequence
		///       this.currentField = expr;
		///       this.state = N;
		///       return true;
		///     with:
		///       yield return expr;
		///       goto blockForState(N);
		///  * Replace the sequence:
		///       this._finally2();
		///       this._finally1();
		///       return false;
		///     with:
		///       yield break;
		///  * Reconstruct try-finally blocks from
		///      (on enter) this.state = N;
		///      (on exit)  this._finallyX();
		/// 
		private BlockContainer ConvertBody(BlockContainer oldBody, StateRangeAnalysis rangeAnalysis)
		{
			var blockStateMap = rangeAnalysis.GetBlockStateSetMapping(oldBody);
			BlockContainer newBody = new BlockContainer();
			// create all new blocks so that they can be referenced by gotos
			for (int blockIndex = 0; blockIndex < oldBody.Blocks.Count; blockIndex++) {
				newBody.Blocks.Add(new Block { ILRange = oldBody.Blocks[blockIndex].ILRange });
			}
			// convert contents of blocks
			
			for (int i = 0; i < oldBody.Blocks.Count; i++) {
				var oldBlock = oldBody.Blocks[i];
				var newBlock = newBody.Blocks[i];
				foreach (var oldInst in oldBlock.Instructions) {
					context.CancellationToken.ThrowIfCancellationRequested();
					if (oldInst.MatchStFld(out var target, out var field, out var value) && target.MatchLdThis()) {
						if (field.MemberDefinition.Equals(stateField)) {
							if (value.MatchLdcI4(out int newState)) {
								// On state change, break up the block:
								// (this allows us to consider each block individually for try-finally reconstruction)
								newBlock = SplitBlock(newBlock, oldInst);
								// We keep the state-changing instruction around (as first instruction of the new block)
								// for reconstructing the try-finallys. 
							} else {
								newBlock.Instructions.Add(new InvalidExpression("Assigned non-constant to iterator.state field") {
									ILRange = oldInst.ILRange
								});
								continue; // don't copy over this instruction, but continue with the basic block
							}
						} else if (field.MemberDefinition.Equals(currentField)) {
							// create yield return
							newBlock.Instructions.Add(new YieldReturn(value) { ILRange = oldInst.ILRange });
							ConvertBranchAfterYieldReturn(newBlock, oldBlock, oldInst.ChildIndex);
							break; // we're done with this basic block
						}
					} else if (oldInst is Call call && call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
						&& finallyMethodToStateRange.ContainsKey((IMethod)call.Method.MemberDefinition))
					{
						// Break up the basic block on a call to a finally method
						// (this allows us to consider each block individually for try-finally reconstruction)
						newBlock = SplitBlock(newBlock, oldInst);
					} else if (oldInst is TryFinally tryFinally && isCompiledWithMono) {
						// with mono, we have to recurse into try-finally blocks
						var oldTryBlock = (BlockContainer)tryFinally.TryBlock;
						var sra = rangeAnalysis.CreateNestedAnalysis();
						sra.AssignStateRanges(oldTryBlock, LongSet.Universe);
						tryFinally.TryBlock = ConvertBody(oldTryBlock, sra);
					}
					// copy over the instruction to the new block
					newBlock.Instructions.Add(oldInst);
					UpdateBranchTargets(oldInst);
				}
			}
			// Insert new artificial block as entry point, and jump to state 0.
			// This causes the method to start directly at the first user code,
			// and the whole compiler-generated state-dispatching logic becomes unreachable code
			// and gets deleted.
			newBody.Blocks.Insert(0, new Block {
				Instructions = { MakeGoTo(0) }
			});
			return newBody;
			void ConvertBranchAfterYieldReturn(Block newBlock, Block oldBlock, int i)
			{
				if (!(oldBlock.Instructions[i + 1].MatchStFld(out var target, out var field, out var value)
					&& target.MatchLdThis()
					&& field.MemberDefinition == stateField
					&& value.MatchLdcI4(out int newState))) {
					newBlock.Instructions.Add(new InvalidBranch("Unable to find new state assignment for yield return"));
					return;
				}
				int pos = i + 2;
				if (oldBlock.Instructions[pos].MatchStLoc(skipFinallyBodies, out value)) {
					if (!value.MatchLdcI4(1)) {
						newBlock.Instructions.Add(new InvalidExpression {
							ExpectedResultType = StackType.Void,
							Message = "Unexpected assignment to skipFinallyBodies"
						});
					}
					pos++;
				}
				if (oldBlock.Instructions[pos].MatchReturn(out var retVal) && retVal.MatchLdcI4(1)) {
					// OK, found return directly after state assignment
				} else if (oldBlock.Instructions[pos].MatchBranch(out var targetBlock) 
					&& targetBlock.Instructions[0].MatchReturn(out retVal) && retVal.MatchLdcI4(1)) {
					// OK, jump to common return block (e.g. on Mono)
				} else {
					newBlock.Instructions.Add(new InvalidBranch("Unable to find 'return true' for yield return"));
					return;
				}
				newBlock.Instructions.Add(MakeGoTo(newState));
			}
			Block SplitBlock(Block newBlock, ILInstruction oldInst)
			{
				if (newBlock.Instructions.Count > 0) {
					var newBlock2 = new Block();
					newBlock2.ILRange = new Interval(oldInst.ILRange.Start, oldInst.ILRange.Start);
					newBody.Blocks.Add(newBlock2);
					newBlock.Instructions.Add(new Branch(newBlock2));
					newBlock = newBlock2;
				}
				return newBlock;
			}
			ILInstruction MakeGoTo(int v)
			{
				Block targetBlock = blockStateMap.GetOrDefault(v);
				if (targetBlock != null) {
					if (targetBlock.Parent == oldBody)
						return new Branch(newBody.Blocks[targetBlock.ChildIndex]);
					else
						return new Branch(targetBlock);
				} else {
					return new InvalidBranch("Could not find block for state " + v);
				}
			}
			void UpdateBranchTargets(ILInstruction inst)
			{
				switch (inst) {
					case Branch branch:
						if (branch.TargetContainer == oldBody) {
							branch.TargetBlock = newBody.Blocks[branch.TargetBlock.ChildIndex];
						}
						break;
					case Leave leave:
						if (leave.MatchReturn(out var value)) {
							if (value.MatchLdLoc(out var v) && v.IsSingleDefinition
								&& v.StoreInstructions.SingleOrDefault() is StLoc stloc) {
								returnStores.Add(stloc);
								value = stloc.Value;
							}
							if (value.MatchLdcI4(0)) {
								// yield break
								leave.ReplaceWith(new Leave(newBody) { ILRange = leave.ILRange });
							} else {
								leave.ReplaceWith(new InvalidBranch("Unexpected return in MoveNext()") { ILRange = leave.ILRange });
							}
						} else {
							if (leave.TargetContainer == oldBody) {
								leave.TargetContainer = newBody;
							}
						}
						break;
				}
				foreach (var child in inst.Children) {
					UpdateBranchTargets(child);
				}
			}
		}
		#endregion
		#region TranslateFieldsToLocalAccess
		/// 
		/// Translates all field accesses in `function` to local variable accesses.
		/// 
		internal static void TranslateFieldsToLocalAccess(ILFunction function, ILInstruction inst, Dictionary fieldToVariableMap)
		{
			if (inst is LdFlda ldflda && ldflda.Target.MatchLdThis()) {
				var fieldDef = (IField)ldflda.Field.MemberDefinition;
				if (!fieldToVariableMap.TryGetValue(fieldDef, out var v)) {
					string name = null;
					if (!string.IsNullOrEmpty(fieldDef.Name) && fieldDef.Name[0] == '<') {
						int pos = fieldDef.Name.IndexOf('>');
						if (pos > 1)
							name = fieldDef.Name.Substring(1, pos - 1);
					}
					v = function.RegisterVariable(VariableKind.Local, ldflda.Field.ReturnType, name);
					v.StateMachineField = ldflda.Field;
					fieldToVariableMap.Add(fieldDef, v);
				}
				if (v.StackType == StackType.Ref) {
					Debug.Assert(v.Kind == VariableKind.Parameter && v.Index < 0); // this pointer
					inst.ReplaceWith(new LdLoc(v) { ILRange = inst.ILRange });
				} else {
					inst.ReplaceWith(new LdLoca(v) { ILRange = inst.ILRange });
				}
			} else if (inst.MatchLdThis()) {
				inst.ReplaceWith(new InvalidExpression("stateMachine") { ExpectedResultType = inst.ResultType, ILRange = inst.ILRange });
			} else {
				foreach (var child in inst.Children) {
					TranslateFieldsToLocalAccess(function, child, fieldToVariableMap);
				}
				if (inst is LdObj ldobj && ldobj.Target is LdLoca ldloca && ldloca.Variable.StateMachineField != null) {
					LdLoc ldloc = new LdLoc(ldloca.Variable);
					ldloc.AddILRange(ldobj.ILRange);
					ldloc.AddILRange(ldloca.ILRange);
					inst.ReplaceWith(ldloc);
				} else if (inst is StObj stobj && stobj.Target is LdLoca ldloca2 && ldloca2.Variable.StateMachineField != null) {
					StLoc stloc = new StLoc(ldloca2.Variable, stobj.Value);
					stloc.AddILRange(stobj.ILRange);
					stloc.AddILRange(ldloca2.ILRange);
					inst.ReplaceWith(stloc);
				}
			}
		}
		#endregion
		#region DecompileFinallyBlocks
		void DecompileFinallyBlocks()
		{
			foreach (var method in finallyMethodToStateRange.Keys) {
				var function = CreateILAst((MethodDefinition)context.TypeSystem.GetCecil(method), context);
				var body = (BlockContainer)function.Body;
				var newState = GetNewState(body.EntryPoint);
				if (newState != null) {
					body.EntryPoint.Instructions.RemoveAt(0);
				}
				function.ReleaseRef(); // make body reusable outside of function
				decompiledFinallyMethods.Add(method, (newState, function));
			}
		}
		#endregion
		#region Reconstruct try-finally blocks
		/// 
		/// Reconstruct try-finally blocks.
		/// * The stateChanges (iterator._state = N;) tell us when to open a try-finally block
		/// * The calls to the finally method tell us when to leave the try block.
		/// 
		/// There might be multiple stateChanges for a given try-finally block, e.g.
		/// both the original entry point, and the target when leaving a nested block.
		/// In proper C# code, the entry point of the try-finally will dominate all other code
		/// in the try-block, so we can use dominance to find the proper entry point.
		/// 
		/// Precondition: the blocks in newBody are topologically sorted.
		/// 
		void ReconstructTryFinallyBlocks(ILFunction iteratorFunction)
		{
			BlockContainer newBody = (BlockContainer)iteratorFunction.Body;
			context.Step("Reconstuct try-finally blocks", newBody);
			var blockState = new int[newBody.Blocks.Count];
			blockState[0] = -1;
			var stateToContainer = new Dictionary();
			stateToContainer.Add(-1, newBody);
			// First, analyse the newBody: for each block, determine the active state number.
			foreach (var block in newBody.Blocks) {
				context.CancellationToken.ThrowIfCancellationRequested();
				int oldState = blockState[block.ChildIndex];
				BlockContainer container; // new container for the block
				if (GetNewState(block) is int newState) {
					// OK, state change
					// Remove the state-changing instruction
					block.Instructions.RemoveAt(0);
					if (!stateToContainer.TryGetValue(newState, out container)) {
						// First time we see this state.
						// This means we just found the entry point of a try block.
						CreateTryBlock(block, newState);
						// CreateTryBlock() wraps the contents of 'block' with a TryFinally.
						// We thus need to put the block (which now contains the whole TryFinally)
						// into the parent container.
						// Assuming a state transition never enters more than one state at once,
						// we can use stateToContainer[oldState] as parent.
						container = stateToContainer[oldState];
					}
				} else {
					// Because newBody is topologically sorted we because we just removed unreachable code,
					// we can assume that blockState[] was already set for this block.
					newState = oldState;
					container = stateToContainer[oldState];
				}
				if (container != newBody) {
					// Move the block into the container.
					container.Blocks.Add(block);
					// Keep the stale reference in newBody.Blocks for now, to avoid
					// changing the ChildIndex of the other blocks while we use it
					// to index the blockState array.
				}
#if DEBUG
				block.Instructions.Insert(0, new Nop { Comment = "state == " + newState });
#endif
				// Propagate newState to successor blocks
				foreach (var branch in block.Descendants.OfType()) {
					if (branch.TargetBlock.Parent == newBody) {
						int stateAfterBranch = newState;
						if (Block.GetPredecessor(branch) is Call call
							&& call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
							&& call.Method.Name == "System.IDisposable.Dispose") {
							// pre-roslyn compiles "yield break;" into "Dispose(); goto return_false;",
							// so convert the dispose call into a state transition to the final state
							stateAfterBranch = -1;
							call.ReplaceWith(new Nop() { Comment = "Dispose call" });
						}
						Debug.Assert(blockState[branch.TargetBlock.ChildIndex] == stateAfterBranch || blockState[branch.TargetBlock.ChildIndex] == 0);
						blockState[branch.TargetBlock.ChildIndex] = stateAfterBranch;
					}
				}
			}
			newBody.Blocks.RemoveAll(b => b.Parent != newBody);
			void CreateTryBlock(Block block, int state)
			{
				var finallyMethod = FindFinallyMethod(state);
				Debug.Assert(finallyMethod != null);
				// remove the method so that it doesn't cause ambiguity when processing nested try-finally blocks
				finallyMethodToStateRange.Remove(finallyMethod);
				var tryBlock = new Block();
				tryBlock.ILRange = block.ILRange;
				tryBlock.Instructions.AddRange(block.Instructions);
				var tryBlockContainer = new BlockContainer();
				tryBlockContainer.Blocks.Add(tryBlock);
				stateToContainer.Add(state, tryBlockContainer);
				ILInstruction finallyBlock;
				if (decompiledFinallyMethods.TryGetValue(finallyMethod, out var decompiledMethod)) {
					finallyBlock = decompiledMethod.function.Body;
					var vars = decompiledMethod.function.Variables.ToArray();
					decompiledMethod.function.Variables.Clear();
					iteratorFunction.Variables.AddRange(vars);
				} else {
					finallyBlock = new InvalidBranch("Missing decompiledFinallyMethod");
				}
				block.Instructions.Clear();
				block.Instructions.Add(new TryFinally(tryBlockContainer, finallyBlock));
			}
			IMethod FindFinallyMethod(int state)
			{
				IMethod foundMethod = null;
				foreach (var (method, stateRange) in finallyMethodToStateRange) {
					if (stateRange.Contains(state)) {
						if (foundMethod == null)
							foundMethod = method;
						else
							Debug.Fail("Ambiguous finally method for state " + state);
					}
				}
				return foundMethod;
			}
		}
		// Gets the state that is transitioned to at the start of the block
		int? GetNewState(Block block)
		{
			if (block.Instructions[0].MatchStFld(out var target, out var field, out var value)
				&& target.MatchLdThis()
				&& field.MemberDefinition.Equals(stateField)
				&& value.MatchLdcI4(out int newState))
			{
				return newState;
			} else if (block.Instructions[0] is Call call
				&& call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
				&& decompiledFinallyMethods.TryGetValue((IMethod)call.Method.MemberDefinition, out var finallyMethod))
			{
				return finallyMethod.outerState;
			}
			return null;
		}
		#endregion
		/// 
		/// Eliminates usage of doFinallyBodies
		/// 
		private void CleanSkipFinallyBodies(ILFunction function)
		{
			if (skipFinallyBodies == null) {
				return; // only mono-compiled code uses skipFinallyBodies
			}
			context.StepStartGroup("CleanSkipFinallyBodies", function);
			Block entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(function.Body as BlockContainer);
			if (skipFinallyBodies.StoreInstructions.Count != 0 || skipFinallyBodies.AddressCount != 0) {
				// misdetected another variable as doFinallyBodies?
				// Fortunately removing the initial store of 0 is harmless, as we
				// default-initialize the variable on uninit uses
				return;
			}
			foreach (var tryFinally in function.Descendants.OfType()) {
				entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(tryFinally.FinallyBlock as BlockContainer);
				if (entryPoint?.Instructions[0] is IfInstruction ifInst) {
					if (ifInst.Condition.MatchLogicNot(out var logicNotArg) && logicNotArg.MatchLdLoc(skipFinallyBodies)) {
						context.Step("Remove if (skipFinallyBodies) from try-finally", tryFinally);
						// condition will always be true now that we're using 'yield' instructions
						entryPoint.Instructions[0] = ifInst.TrueInst;
						entryPoint.Instructions.RemoveRange(1, entryPoint.Instructions.Count - 1);
					}
				}
			}
			context.StepEndGroup(keepIfEmpty: true);
		}
	}
}