// 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 System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection.Metadata; 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; MetadataReader metadata; /// The type that contains the function being decompiled. TypeDefinitionHandle currentType; /// The compiler-generated enumerator class. /// Set in MatchEnumeratorCreationPattern() TypeDefinitionHandle enumeratorType; /// The constructor of the compiler-generated enumerator class. /// Set in MatchEnumeratorCreationPattern() MethodDefinitionHandle enumeratorCtor; /// Set in MatchEnumeratorCreationPattern() bool isCompiledWithMono; /// The dispose method of the compiler-generated enumerator class. /// Set in ConstructExceptionTable() MethodDefinitionHandle 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; /// The disposing field of the compiler-generated enumerator class. /// Set in ConstructExceptionTable() for assembly compiled with Mono IField disposingField; /// 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.metadata = context.PEFile.Metadata; this.currentType = metadata.GetMethodDefinition((MethodDefinitionHandle)context.Function.Method.MetadataToken).GetDeclaringType(); this.enumeratorType = default; this.enumeratorCtor = default; this.stateField = null; this.currentField = null; this.disposingField = 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(function); } catch (SymbolicAnalysisFailedException) { return; } context.Step("Replacing body with MoveNext() body", function); function.IsIterator = true; function.StateMachineCompiledWithMono = isCompiledWithMono; function.Body = newBody; // register any locals used in newBody function.Variables.AddRange(newBody.Descendants.OfType().Select(inst => inst.Variable).Distinct()); 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); 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); } // We need to clean up nested blocks before the main block, so that edges from unreachable code // in nested containers into the main container are removed before we clean up the main container. } // Note: because this only deletes blocks outright, the 'stateChanges' entries remain valid // (though some may point to now-deleted blocks) newBody.SortBlocks(deleteUnreachableBlocks: true); function.CheckInvariant(ILPhase.Normal); if (!isCompiledWithMono) { DecompileFinallyBlocks(); ReconstructTryFinallyBlocks(function); } context.Step("Translate fields to local accesses", function); TranslateFieldsToLocalAccess(function, function, fieldToParameterMap, isCompiledWithMono); 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) { return MatchEnumeratorCreationNewObj(inst, metadata, currentType, out enumeratorCtor, out enumeratorType); } internal static bool MatchEnumeratorCreationNewObj(ILInstruction inst, MetadataReader metadata, TypeDefinitionHandle currentType, out MethodDefinitionHandle enumeratorCtor, out TypeDefinitionHandle enumeratorType) { enumeratorCtor = default; enumeratorType = default; // 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; var handle = newObj.Method.MetadataToken; enumeratorCtor = handle.IsNil || handle.Kind != HandleKind.MethodDefinition ? default : (MethodDefinitionHandle)handle; enumeratorType = enumeratorCtor.IsNil ? default : metadata.GetMethodDefinition(enumeratorCtor).GetDeclaringType(); return (enumeratorType.IsNil ? default : metadata.GetTypeDefinition(enumeratorType).GetDeclaringType()) == currentType && IsCompilerGeneratorEnumerator(enumeratorType, metadata); } 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; var handle = newObj.Method.MetadataToken; enumeratorCtor = handle.IsNil || handle.Kind != HandleKind.MethodDefinition ? default : (MethodDefinitionHandle)handle; enumeratorType = enumeratorCtor.IsNil ? default : metadata.GetMethodDefinition(enumeratorCtor).GetDeclaringType(); return (enumeratorType.IsNil ? default : metadata.GetTypeDefinition(enumeratorType).GetDeclaringType()) == currentType && IsCompilerGeneratorEnumerator(enumeratorType, metadata); } public static bool IsCompilerGeneratorEnumerator(TypeDefinitionHandle type, MetadataReader metadata) { TypeDefinition td; if (type.IsNil || !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil) return false; foreach (var i in td.GetInterfaceImplementations()) { var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata); if (!tr.IsNested && tr.TopLevelTypeName.Namespace == "System.Collections" && tr.TopLevelTypeName.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(MethodDefinitionHandle method, ILTransformContext context) { var metadata = context.PEFile.Metadata; if (method.IsNil) throw new SymbolicAnalysisFailedException(); var methodDef = metadata.GetMethodDefinition(method); if (!methodDef.HasBody()) throw new SymbolicAnalysisFailedException(); GenericContext genericContext = context.Function.GenericContext; genericContext = new GenericContext( classTypeParameters: (genericContext.ClassTypeParameters ?? EmptyList.Instance) .Concat(genericContext.MethodTypeParameters ?? EmptyList.Instance).ToArray(), methodTypeParameters: null); var body = context.TypeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress); var il = context.CreateILReader() .ReadIL(method, body, genericContext, ILFunctionKind.TopLevelFunction, context.CancellationToken); il.RunTransforms(CSharpDecompiler.EarlyILTransforms(true), new ILTransformContext(il, context.TypeSystem, context.DebugInfo, 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() { MethodDefinitionHandle getCurrentMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault( m => metadata.GetString(metadata.GetMethodDefinition(m).Name).StartsWith("System.Collections.Generic.IEnumerator", StringComparison.Ordinal) && metadata.GetString(metadata.GetMethodDefinition(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() { MethodDefinitionHandle getEnumeratorMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault( m => metadata.GetString(metadata.GetMethodDefinition(m).Name).StartsWith("System.Collections.Generic.IEnumerable", StringComparison.Ordinal) && metadata.GetString(metadata.GetMethodDefinition(m).Name).EndsWith(".GetEnumerator", StringComparison.Ordinal)); ResolveIEnumerableIEnumeratorFieldMapping(getEnumeratorMethod, context, fieldToParameterMap); } internal static void ResolveIEnumerableIEnumeratorFieldMapping(MethodDefinitionHandle getEnumeratorMethod, ILTransformContext context, Dictionary fieldToParameterMap) { if (getEnumeratorMethod.IsNil) 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) { disposeMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(m => metadata.GetString(metadata.GetMethodDefinition(m).Name) == "Dispose"); var function = CreateILAst(disposeMethod, context); BlockContainer body = (BlockContainer)function.Body; for (var i = 0; (i < body.EntryPoint.Instructions.Count) && !(body.EntryPoint.Instructions[i] is Branch); i++) { if (body.EntryPoint.Instructions[i] is StObj stobj && stobj.MatchStFld(out var target, out var field, out var value) && target.MatchLdThis() && field.Type.IsKnownType(KnownTypeCode.Boolean) && value.MatchLdcI4(1)) { disposingField = (IField)field.MemberDefinition; break; } } // On mono, we don't need to analyse Dispose() to reconstruct the try-finally structure. finallyMethodToStateRange = default; } else { // Non-Mono: analyze try-finally structure in Dispose() disposeMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(m => metadata.GetString(metadata.GetMethodDefinition(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(ILFunction function) { context.StepStartGroup("AnalyzeMoveNext"); MethodDefinitionHandle moveNextMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(m => metadata.GetString(metadata.GetMethodDefinition(m).Name) == "MoveNext"); ILFunction moveNextFunction = CreateILAst(moveNextMethod, context); function.MoveNextMethod = moveNextFunction.Method; function.SequencePointCandidates = moveNextFunction.SequencePointCandidates; function.CodeSize = moveNextFunction.CodeSize; // 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 && call.Method.MetadataToken == 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().WithILRange(oldBody); // 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().WithILRange(oldBody.Blocks[blockIndex])); } // 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").WithILRange(oldInst)); 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).WithILRange(oldInst)); ConvertBranchAfterYieldReturn(newBlock, oldBlock, oldInst.ChildIndex + 1); 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); newBlock.AddILRange(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 pos) { Block targetBlock; if (isCompiledWithMono && disposingField != null) { // Mono skips over the state assignment if 'this.disposing' is set: // ... // stfld $current(ldloc this, yield-expr) // if (ldfld $disposing(ldloc this)) br IL_007c // br targetBlock // } // // Block targetBlock (incoming: 1) { // stfld $PC(ldloc this, ldc.i4 1) // br setSkipFinallyBodies // } // // Block setSkipFinallyBodies (incoming: 2) { // stloc skipFinallyBodies(ldc.i4 1) // br returnBlock // } if (oldBlock.Instructions[pos].MatchIfInstruction(out var condition, out _) && condition.MatchLdFld(out var condTarget, out var condField) && condTarget.MatchLdThis() && condField.MemberDefinition.Equals(disposingField) && oldBlock.Instructions[pos + 1].MatchBranch(out targetBlock) && targetBlock.Parent == oldBlock.Parent) { // Keep looking at the target block: oldBlock = targetBlock; pos = 0; } } if (oldBlock.Instructions[pos].MatchStFld(out var target, out var field, out var value) && target.MatchLdThis() && field.MemberDefinition == stateField && value.MatchLdcI4(out int newState)) { pos++; } else { newBlock.Instructions.Add(new InvalidBranch("Unable to find new state assignment for yield return")); return; } // Mono may have 'br setSkipFinallyBodies' here, so follow the branch if (oldBlock.Instructions[pos].MatchBranch(out targetBlock) && targetBlock.Parent == oldBlock.Parent) { oldBlock = targetBlock; pos = 0; } 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 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.AddILRange(new Interval(oldInst.StartILOffset, oldInst.StartILOffset)); 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)) { bool validYieldBreak = value.MatchLdcI4(0); if (value.MatchLdLoc(out var v) && (v.Kind == VariableKind.Local || v.Kind == VariableKind.StackSlot) && v.StoreInstructions.All(store => store is StLoc stloc && stloc.Value.MatchLdcI4(0))) { validYieldBreak = true; returnStores.AddRange(v.StoreInstructions.Cast()); } if (validYieldBreak) { // yield break leave.ReplaceWith(new Leave(newBody).WithILRange(leave)); } else { leave.ReplaceWith(new InvalidBranch("Unexpected return in MoveNext()").WithILRange(leave)); } } 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, bool isCompiledWithMono = false) { 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.HasInitialValue = true; // the field was default-initialized, so keep those semantics for the variable 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).WithILRange(inst)); } else { inst.ReplaceWith(new LdLoca(v).WithILRange(inst)); } } else if (!isCompiledWithMono && inst.MatchLdThis()) { inst.ReplaceWith(new InvalidExpression("stateMachine") { ExpectedResultType = inst.ResultType }.WithILRange(inst)); } else { foreach (var child in inst.Children) { TranslateFieldsToLocalAccess(function, child, fieldToVariableMap, isCompiledWithMono); } if (inst is LdObj ldobj && ldobj.Target is LdLoca ldloca && ldloca.Variable.StateMachineField != null) { LdLoc ldloc = new LdLoc(ldloca.Variable); ldloc.AddILRange(ldobj); ldloc.AddILRange(ldloca); 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); stloc.AddILRange(ldloca2); inst.ReplaceWith(stloc); } } } #endregion #region DecompileFinallyBlocks void DecompileFinallyBlocks() { foreach (var method in finallyMethodToStateRange.Keys) { var function = CreateILAst((MethodDefinitionHandle)method.MetadataToken, 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.AddILRange(block); tryBlock.Instructions.AddRange(block.Instructions); var tryBlockContainer = new BlockContainer(); tryBlockContainer.Blocks.Add(tryBlock); tryBlockContainer.AddILRange(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).WithILRange(tryBlockContainer)); } 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); } } }