From 5621101436821c94abffe8bdb3a742cdd2a76e61 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 25 Dec 2016 15:18:31 +0100 Subject: [PATCH] Reconstruct try-finally blocks in yield return decompiler --- .../CSharp/StatementBuilder.cs | 36 ++-- .../FlowAnalysis/DataFlowVisitor.cs | 3 +- .../ICSharpCode.Decompiler.csproj | 1 - ICSharpCode.Decompiler/IL/BlockBuilder.cs | 23 +-- .../IL/ControlFlow/ControlFlowGraph.cs | 6 +- .../ControlFlow/ControlFlowSimplification.cs | 11 +- .../IL/ControlFlow/ExitPoints.cs | 28 +-- .../IL/ControlFlow/StateRangeAnalysis.cs | 2 +- .../IL/ControlFlow/YieldReturnDecompiler.cs | 189 ++++++++++++++++-- ICSharpCode.Decompiler/IL/ILReader.cs | 8 +- ICSharpCode.Decompiler/IL/Instructions.cs | 83 +++++++- ICSharpCode.Decompiler/IL/Instructions.tt | 5 +- .../IL/Instructions/Leave.cs | 11 + .../IL/Instructions/PatternMatching.cs | 11 - .../IL/Instructions/Return.cs | 115 ----------- .../Tests/TestCases/Pretty/YieldReturn.cs | 151 ++++++++++++++ .../Util/CollectionExtensions.cs | 6 + 17 files changed, 475 insertions(+), 214 deletions(-) delete mode 100644 ICSharpCode.Decompiler/IL/Instructions/Return.cs diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index e3c5b5dc9..d89f73b38 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -148,7 +148,7 @@ namespace ICSharpCode.Decompiler.CSharp { if (inst.TargetContainer == breakTarget) return new BreakStatement(); - if (inst.TargetContainer.SlotInfo == ILFunction.BodySlot) { + if (inst.IsLeavingFunction) { if (currentFunction.IsIterator) return new YieldBreakStatement(); else @@ -174,9 +174,7 @@ namespace ICSharpCode.Decompiler.CSharp protected internal override Statement VisitReturn(Return inst) { - if (inst.ReturnValue == null) - return new ReturnStatement(); - return new ReturnStatement(exprBuilder.Translate(inst.ReturnValue).ConvertTo(currentMethod.ReturnType, exprBuilder)); + return new ReturnStatement(exprBuilder.Translate(inst.Value).ConvertTo(currentMethod.ReturnType, exprBuilder)); } protected internal override Statement VisitYieldReturn(YieldReturn inst) @@ -298,7 +296,9 @@ namespace ICSharpCode.Decompiler.CSharp // and set the continueTarget to correctly convert 'br incrementBlock' instructions to continue; Block incrementBlock = null; if (container.EntryPoint.IncomingEdgeCount == 2) { - incrementBlock = container.Blocks.SingleOrDefault(b => b.Instructions.Last().MatchBranch(container.EntryPoint) && b.Instructions.All(IsSimpleStatement)); + incrementBlock = container.Blocks.SingleOrDefault( + b => b.Instructions.Last().MatchBranch(container.EntryPoint) + && b.Instructions.SkipLast(1).All(IsSimpleStatement)); if (incrementBlock != null) continueTarget = incrementBlock; } @@ -354,18 +354,23 @@ namespace ICSharpCode.Decompiler.CSharp stmt.Remove(); return new WhileStatement(conditionExpr, blockStatement); } - + + /// + /// Gets whether the statement is 'simple' (usable as for loop iterator): + /// Currently we only accept calls and assignments. + /// private static bool IsSimpleStatement(ILInstruction inst) { - switch (inst) { - case IfInstruction i: - case SwitchInstruction s: - case TryCatch t: - case TryFault fa: - case TryFinally fi: - return false; - default: + switch (inst.OpCode) { + case OpCode.Call: + case OpCode.CallVirt: + case OpCode.NewObj: + case OpCode.StLoc: + case OpCode.StObj: + case OpCode.CompoundAssignmentInstruction: return true; + default: + return false; } } @@ -431,7 +436,8 @@ namespace ICSharpCode.Decompiler.CSharp if (leave.ChildIndex != block.Instructions.Count - 1 || block.FinalInstruction.OpCode != OpCode.Nop) return false; BlockContainer container = (BlockContainer)block.Parent; - return block.ChildIndex == container.Blocks.Count - 1; + return block.ChildIndex == container.Blocks.Count - 1 + && container == leave.TargetContainer; } } } diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs index cc9046710..36a6b26d0 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs @@ -430,8 +430,7 @@ namespace ICSharpCode.Decompiler.FlowAnalysis protected internal override void VisitReturn(Return inst) { - if (inst.ReturnValue != null) - inst.ReturnValue.AcceptVisitor(this); + inst.Value.AcceptVisitor(this); MarkUnreachable(); } diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 0696fc373..6d6db5677 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -320,7 +320,6 @@ - diff --git a/ICSharpCode.Decompiler/IL/BlockBuilder.cs b/ICSharpCode.Decompiler/IL/BlockBuilder.cs index 929f9876d..98e8fd287 100644 --- a/ICSharpCode.Decompiler/IL/BlockBuilder.cs +++ b/ICSharpCode.Decompiler/IL/BlockBuilder.cs @@ -107,10 +107,9 @@ namespace ICSharpCode.Decompiler.IL Block currentBlock; Stack containerStack = new Stack(); - public BlockContainer CreateBlocks(List instructions, BitArray incomingBranches) + public void CreateBlocks(BlockContainer mainContainer, List instructions, BitArray incomingBranches) { CreateContainerStructure(); - var mainContainer = new BlockContainer(); mainContainer.ILRange = new Interval(0, body.CodeSize); currentContainer = mainContainer; @@ -155,7 +154,6 @@ namespace ICSharpCode.Decompiler.IL FinalizeCurrentBlock(body.CodeSize, fallthrough: false); containerStack.Clear(); ConnectBranches(mainContainer); - return mainContainer; } private void FinalizeCurrentBlock(int currentILOffset, bool fallthrough) @@ -170,19 +168,20 @@ namespace ICSharpCode.Decompiler.IL void ConnectBranches(ILInstruction inst) { - switch (inst.OpCode) { - case OpCode.Branch: - var branch = (Branch)inst; + switch (inst) { + case Branch branch: Debug.Assert(branch.TargetBlock == null); branch.TargetBlock = FindBranchTarget(branch.TargetILOffset); break; - case OpCode.Leave: - var leave = (Leave)inst; - Debug.Assert(leave.TargetContainer == null); - leave.TargetContainer = containerStack.Peek(); + case Leave leave: + // ret (in void method) = leave(mainContainer) + // endfinally = leave(null) + if (leave.TargetContainer == null) { + // assign the finally/filter container + leave.TargetContainer = containerStack.Peek(); + } break; - case OpCode.BlockContainer: - var container = (BlockContainer)inst; + case BlockContainer container: containerStack.Push(container); foreach (var block in container.Blocks) { ConnectBranches(block); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowGraph.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowGraph.cs index d0a488f29..ca40a2029 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowGraph.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowGraph.cs @@ -100,7 +100,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow // Leave instructions (like other exits out of the container) // are ignored for the CFG and dominance, // but is relevant for HasReachableExit(). - nodeHasDirectExitOutOfContainer.Set(i); + // However, a 'leave' that exits the whole function represents a void return, + // and is not considered a reachable exit (just like non-void returns). + if (!(leave.TargetContainer.Parent is ILFunction)) { + nodeHasDirectExitOutOfContainer.Set(i); + } } } } diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs index 031a66015..c317a4bb5 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs @@ -64,12 +64,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Return ret = (Return)block.Instructions[1]; ILVariable v; ILInstruction inst; - if (ret.ReturnValue != null && ret.ReturnValue.MatchLdLoc(out v) + if (ret.Value.MatchLdLoc(out v) && v.IsSingleDefinition && v.LoadCount == 1 && block.Instructions[0].MatchStLoc(v, out inst)) { - inst.AddILRange(ret.ReturnValue.ILRange); + inst.AddILRange(ret.Value.ILRange); inst.AddILRange(block.Instructions[0].ILRange); - ret.ReturnValue = inst; + ret.Value = inst; block.Instructions.RemoveAt(0); } } @@ -127,8 +127,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { if (targetBlock.Instructions.Count != 1 || targetBlock.FinalInstruction.OpCode != OpCode.Nop) return false; - var ret = targetBlock.Instructions[0] as Return; - return ret != null && (ret.ReturnValue == null || ret.ReturnValue.OpCode == OpCode.LdLoc); + var inst = targetBlock.Instructions[0]; + return (inst is Return ret && ret.Value is LdLoc + || inst is Leave leave && leave.IsLeavingFunction); } static bool CombineBlockWithNextBlock(BlockContainer container, Block block) diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs index 689d8aa4e..320b4007f 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs @@ -26,7 +26,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow /// Detect suitable exit points for BlockContainers. /// /// An "exit point" is an instruction that causes control flow - /// to leave the container (a branch, leave or value-less return instruction). + /// to leave the container (a branch or leave instruction). /// /// If an "exit point" instruction is placed immediately following a /// block container, each equivalent exit point within the container @@ -49,10 +49,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { static readonly Nop ExitNotYetDetermined = new Nop(); static readonly Nop NoExit = new Nop(); - // The special ReturnExit value (indicating fall-through out of a void method) - // is considered a compatible exit point with all normal return instructions. - static readonly Return ReturnExit = new Return(); - + /// /// Gets the next instruction after is executed. /// Returns NoExit when the next instruction cannot be identified; @@ -70,15 +67,13 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow || slot == PinnedRegion.BodySlot) { return GetExit(inst.Parent); - } else if (slot == ILFunction.BodySlot) { - return ReturnExit; } return NoExit; } /// /// Returns true iff exit1 and exit2 are both exit instructions - /// (branch, leave, or return) and both represent the same exit. + /// (branch or leave) and both represent the same exit. /// internal static bool CompatibleExitInstruction(ILInstruction exit1, ILInstruction exit2) { @@ -93,10 +88,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Leave leave1 = (Leave)exit1; Leave leave2 = (Leave)exit2; return leave1.TargetContainer == leave2.TargetContainer; - case OpCode.Return: - Return ret1 = (Return)exit1; - Return ret2 = (Return)exit2; - return ret1.ReturnValue == null && ret2.ReturnValue == null; default: return false; } @@ -159,7 +150,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow void HandleExit(ILInstruction inst) { - if (currentExit == ExitNotYetDetermined && !(inst is Return)) { + if (currentExit == ExitNotYetDetermined && !(inst is Leave l && l.IsLeavingFunction)) { currentExit = inst; inst.ReplaceWith(new Leave(currentContainer) { ILRange = inst.ILRange }); } else if (CompatibleExitInstruction(inst, currentExit)) { @@ -178,16 +169,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { HandleExit(inst); } - - protected internal override void VisitReturn(Return inst) - { - if (inst.ReturnValue == null) { - // only void returns are considered exit points - HandleExit(inst); - } else { - base.VisitReturn(inst); - } - } } /* @@ -212,7 +193,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow void Visit(ILInstruction inst) { switch (inst.OpCode) { - case OpCode.Return: case OpCode.Leave: case OpCode.Branch: if (DetectExitPoints.CompatibleExitInstruction(inst, exitPoint)) { diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs b/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs index ded58e325..bfcac5ae3 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs @@ -169,7 +169,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow return LongSet.Empty; // return Empty since we executed user code (the finally method) default: // User code - abort analysis - if (mode == StateRangeAnalysisMode.IteratorDispose && inst.OpCode != OpCode.Return) { + if (mode == StateRangeAnalysisMode.IteratorDispose && !(inst is Leave l && l.IsLeavingFunction)) { throw new SymbolicAnalysisFailedException("Unexpected instruction in Iterator.Dispose()"); } return LongSet.Empty; diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs index f4ef21c70..155a5aff7 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.FlowAnalysis; using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; @@ -77,10 +78,18 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow /// 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(); + + /* /// /// List of blocks that change the iterator state on block entry. /// readonly List<(int state, Block block)> stateChanges = new List<(int state, Block block)>(); + */ #region Run() method public void Run(ILFunction function, ILTransformContext context) @@ -95,7 +104,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow this.currentField = null; this.fieldToParameterMap.Clear(); this.finallyMethodToStateRange = null; - this.stateChanges.Clear(); + this.decompiledFinallyMethods.Clear(); if (!MatchEnumeratorCreationPattern(function)) return; BlockContainer newBody; @@ -123,7 +132,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow // (though some may point to now-deleted blocks) newBody.SortBlocks(deleteUnreachableBlocks: true); - context.Step("Reconstruct try-finally blocks", function); + DecompileFinallyBlocks(); ReconstructTryFinallyBlocks(newBody); context.Step("Translate fields to local accesses", function); @@ -440,35 +449,34 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow 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 (if necessary): - 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; - } -#if DEBUG - newBlock.Instructions.Add(new Nop { Comment = "iterator._state = " + newState }); -#endif - stateChanges.Add((newState, newBlock)); + // 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 } - 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.MatchReturn(out value)) { if (value.MatchLdLoc(out var v)) { ssaDefs.TryGetValue(v, out value); } - if (value.MatchLdcI4(0)) { + if (value != null && value.MatchLdcI4(0)) { // yield break newBlock.Instructions.Add(new Leave(newBody) { ILRange = oldInst.ILRange }); } else { @@ -510,6 +518,18 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow 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 = null; @@ -573,13 +593,146 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } #endregion + #region DecompileFinallyBlocks + void DecompileFinallyBlocks() + { + foreach (var method in finallyMethodToStateRange.Keys) { + var function = CreateILAst((MethodDefinition)context.TypeSystem.GetCecil(method)); + 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, body)); + } + } + #endregion + #region Reconstruct try-finally blocks - private void ReconstructTryFinallyBlocks(BlockContainer newBody) + /// + /// 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(BlockContainer newBody) { - // TODO + context.Stepper.Step("Reconstuct try-finally blocks"); + 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) { + 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) { + Debug.Assert(blockState[branch.TargetBlock.ChildIndex] == newState || blockState[branch.TargetBlock.ChildIndex] == 0); + blockState[branch.TargetBlock.ChildIndex] = newState; + } + } + } + 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 get 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.body; + } 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 } } diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 27fd2aea3..1610ee905 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -53,6 +53,7 @@ namespace ICSharpCode.Decompiler.IL ILVariable[] parameterVariables; ILVariable[] localVariables; BitArray isBranchTarget; + BlockContainer mainContainer; List instructionBuilder; // Dictionary that stores stacks for each IL instruction @@ -78,6 +79,7 @@ namespace ICSharpCode.Decompiler.IL v.HasInitialValue = true; } } + mainContainer = new BlockContainer(); this.instructionBuilder = new List(); this.isBranchTarget = new BitArray(body.CodeSize); this.stackByOffset = new Dictionary>(); @@ -292,8 +294,8 @@ namespace ICSharpCode.Decompiler.IL Init(body); ReadInstructions(cancellationToken); var blockBuilder = new BlockBuilder(body, typeSystem, variableByExceptionHandler); - var container = blockBuilder.CreateBlocks(instructionBuilder, isBranchTarget); - var function = new ILFunction(body.Method, container); + blockBuilder.CreateBlocks(mainContainer, instructionBuilder, isBranchTarget); + var function = new ILFunction(body.Method, mainContainer); CollectionExtensions.AddRange(function.Variables, parameterVariables); CollectionExtensions.AddRange(function.Variables, localVariables); CollectionExtensions.AddRange(function.Variables, stackVariables); @@ -920,7 +922,7 @@ namespace ICSharpCode.Decompiler.IL private ILInstruction Return() { if (methodReturnStackType == StackType.Void) - return new IL.Return(); + return new IL.Leave(mainContainer); else return new IL.Return(Pop(methodReturnStackType)); } diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 734f841a8..cccf8d153 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -113,7 +113,7 @@ namespace ICSharpCode.Decompiler.IL LdMemberToken, /// Allocates space in the stack frame LocAlloc, - /// Returns from the current method or lambda. + /// Returns from the current method or lambda. Only used when returning a value; void returns are represented using a 'leave' instruction. Return, /// Load address of instance field LdFlda, @@ -2443,10 +2443,77 @@ namespace ICSharpCode.Decompiler.IL } namespace ICSharpCode.Decompiler.IL { - /// Returns from the current method or lambda. + /// Returns from the current method or lambda. Only used when returning a value; void returns are represented using a 'leave' instruction. public sealed partial class Return : ILInstruction { + public Return(ILInstruction value) : base(OpCode.Return) + { + this.Value = value; + } + public static readonly SlotInfo ValueSlot = new SlotInfo("Value", canInlineInto: true); + ILInstruction value; + public ILInstruction Value { + get { return this.value; } + set { + ValidateChild(value); + SetChildInstruction(ref this.value, value, 0); + } + } + protected sealed override int GetChildCount() + { + return 1; + } + protected sealed override ILInstruction GetChild(int index) + { + switch (index) { + case 0: + return this.value; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override void SetChild(int index, ILInstruction value) + { + switch (index) { + case 0: + this.Value = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override SlotInfo GetChildSlot(int index) + { + switch (index) { + case 0: + return ValueSlot; + default: + throw new IndexOutOfRangeException(); + } + } + public sealed override ILInstruction Clone() + { + var clone = (Return)ShallowClone(); + clone.Value = this.value.Clone(); + return clone; + } public override StackType ResultType { get { return StackType.Void; } } + protected override InstructionFlags ComputeFlags() + { + return value.Flags | InstructionFlags.MayBranch | InstructionFlags.EndPointUnreachable; + } + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.MayBranch | InstructionFlags.EndPointUnreachable; + } + } + public override void WriteTo(ITextOutput output) + { + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output); + output.Write(')'); + } public override void AcceptVisitor(ILVisitor visitor) { visitor.VisitReturn(this); @@ -2462,7 +2529,7 @@ namespace ICSharpCode.Decompiler.IL protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) { var o = other as Return; - return o != null && this.hasArgument == o.hasArgument && (!hasArgument || this.ReturnValue.PerformMatch(o.ReturnValue, ref match)); + return o != null && this.value.PerformMatch(o.value, ref match); } } } @@ -5018,6 +5085,16 @@ namespace ICSharpCode.Decompiler.IL argument = default(ILInstruction); return false; } + public bool MatchReturn(out ILInstruction value) + { + var inst = this as Return; + if (inst != null) { + value = inst.Value; + return true; + } + value = default(ILInstruction); + return false; + } public bool MatchLdFlda(out ILInstruction target, out IField field) { var inst = this as LdFlda; diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index d51e325be..a735d806e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -158,9 +158,8 @@ CustomClassName("LdMemberToken"), NoArguments, HasMemberOperand, ResultType("O")), new OpCode("localloc", "Allocates space in the stack frame", CustomClassName("LocAlloc"), Unary, ResultType("I"), MayThrow), - new OpCode("ret", "Returns from the current method or lambda.", - CustomClassName("Return"), CustomConstructor, CustomComputeFlags, MayBranch, UnconditionalBranch, - MatchCondition("this.hasArgument == o.hasArgument && (!hasArgument || this.ReturnValue.PerformMatch(o.ReturnValue, ref match))")), + new OpCode("ret", "Returns from the current method or lambda. Only used when returning a value; void returns are represented using a 'leave' instruction.", + CustomClassName("Return"), CustomArguments("value"), MayBranch, UnconditionalBranch), new OpCode("ldflda", "Load address of instance field", CustomClassName("LdFlda"), CustomArguments("target"), MayThrowIfNotDelayed, HasFieldOperand, ResultType("Ref")), diff --git a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs index e8856ca31..ab9539c11 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs @@ -81,6 +81,17 @@ namespace ICSharpCode.Decompiler.IL get { return targetContainer != null ? targetContainer.EntryPoint.Label : string.Empty; } } + /// + /// Gets whether the leave instruction is leaving the whole ILFunction. + /// (TargetContainer == main container of the function). + /// + /// This is only valid for functions returning void (representing value-less "return;"), + /// and for iterators (representing "yield break;"). + /// + public bool IsLeavingFunction { + get { return targetContainer?.Parent is ILFunction; } + } + internal override void CheckInvariant(ILPhase phase) { base.CheckInvariant(phase); diff --git a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs index 53dc2465b..b3f6b94f1 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs @@ -111,17 +111,6 @@ namespace ICSharpCode.Decompiler.IL var inst = this as Leave; return inst != null && inst.TargetContainer == targetContainer; } - - public bool MatchReturn(out ILInstruction returnValue) - { - if (this is Return ret) { - returnValue = ret.ReturnValue; - return returnValue != null; - } else { - returnValue = null; - return false; - } - } public bool MatchIfInstruction(out ILInstruction condition, out ILInstruction trueInst, out ILInstruction falseInst) { diff --git a/ICSharpCode.Decompiler/IL/Instructions/Return.cs b/ICSharpCode.Decompiler/IL/Instructions/Return.cs deleted file mode 100644 index f3aa34457..000000000 --- a/ICSharpCode.Decompiler/IL/Instructions/Return.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2014 Daniel Grunwald -// -// 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.Diagnostics; - -namespace ICSharpCode.Decompiler.IL -{ - partial class Return - { - public static readonly SlotInfo ReturnValueSlot = new SlotInfo("ReturnValue", canInlineInto: true); - - readonly bool hasArgument; - ILInstruction returnValue; - - /// - /// The value to return. Null if this return statement is within a void method. - /// - public ILInstruction ReturnValue { - get { return returnValue; } - set { - if (hasArgument) { - ValidateChild(value); - SetChildInstruction(ref returnValue, value, 0); - } else { - Debug.Assert(value == null); - } - } - } - - public Return() : base(OpCode.Return) - { - } - - public Return(ILInstruction argument) : base(OpCode.Return) - { - this.hasArgument = true; - this.ReturnValue = argument; - } - - public override ILInstruction Clone() - { - Return clone = (Return)ShallowClone(); - if (hasArgument) - clone.ReturnValue = returnValue.Clone(); - return clone; - } - - protected override InstructionFlags ComputeFlags() - { - InstructionFlags flags = InstructionFlags.MayBranch | InstructionFlags.EndPointUnreachable; - if (hasArgument) { - flags |= returnValue.Flags; - } - return flags; - } - - public override InstructionFlags DirectFlags { - get { - return InstructionFlags.MayBranch | InstructionFlags.EndPointUnreachable; - } - } - - public override void WriteTo(ITextOutput output) - { - output.Write(OpCode); - if (returnValue != null) { - output.Write('('); - returnValue.WriteTo(output); - output.Write(')'); - } - } - - protected override int GetChildCount() - { - return hasArgument ? 1 : 0; - } - - protected override ILInstruction GetChild(int index) - { - if (index == 0 && hasArgument) - return returnValue; - else - throw new IndexOutOfRangeException(); - } - - protected override void SetChild(int index, ILInstruction value) - { - if (index == 0 && hasArgument) - ReturnValue = value; - else - throw new IndexOutOfRangeException(); - } - - protected override SlotInfo GetChildSlot(int index) - { - return ReturnValueSlot; - } - } -} diff --git a/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs b/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs index 0bc27cd13..64339cdca 100644 --- a/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs +++ b/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs @@ -190,5 +190,156 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Console.WriteLine("outer finally"); } } + + public static IEnumerable YieldBreakInCatch() + { + yield return 0; + try { + Console.WriteLine("In Try"); + } catch { + // yield return is not allowed in catch, but yield break is + yield break; + } + yield return 1; + } + + public static IEnumerable YieldBreakInCatchInTryFinally() + { + try { + yield return 0; + try { + Console.WriteLine("In Try"); + } catch { + // yield return is not allowed in catch, but yield break is + yield break; + } + } finally { + Console.WriteLine("Finally"); + } + } + + public static IEnumerable YieldBreakInTryCatchInTryFinally() + { + try { + yield return 0; + try { + Console.WriteLine("In Try"); + yield break; + } catch { + Console.WriteLine("Catch"); + } + } finally { + Console.WriteLine("Finally"); + } + } + + public static IEnumerable YieldBreakInTryFinallyInTryFinally() + { + try { + yield return 0; + try { + Console.WriteLine("In Try"); + yield break; + } finally { + Console.WriteLine("Inner Finally"); + } + } finally { + Console.WriteLine("Finally"); + } + } + + public static IEnumerable UnconditionalThrowInTryFinally() + { + // Here, MoveNext() doesn't call the finally methods at all + // (only indirectly via Dispose()) + try { + yield return 0; + throw new NotImplementedException(); + } finally { + Console.WriteLine("Finally"); + } + } + + public static IEnumerable NestedTryFinallyStartingOnSamePosition() + { + // The first user IL instruction is already in 2 nested try blocks. + try { + try { + yield return 0; + } finally { + Console.WriteLine("Inner Finally"); + } + } finally { + Console.WriteLine("Outer Finally"); + } + } + + + public static IEnumerable TryFinallyWithTwoExitPoints(bool b) + { + // The first user IL instruction is already in 2 nested try blocks. + try { + if (b) { + yield return 1; + goto exit1; + } else { + yield return 2; + goto exit2; + } + } finally { + Console.WriteLine("Finally"); + } + exit1: + Console.WriteLine("Exit1"); + yield break; + exit2: + Console.WriteLine("Exit2"); + } + + public static IEnumerable TryFinallyWithTwoExitPointsInNestedTry(bool b) + { + // The first user IL instruction is already in 2 nested try blocks. + try { + yield return 1; + try { + if (b) + goto exit1; + else + goto exit2; + } catch { + Console.WriteLine("Catch"); + } + } finally { + Console.WriteLine("Finally"); + } + exit1: + Console.WriteLine("Exit1"); + yield break; + exit2: + Console.WriteLine("Exit2"); + } + + public static IEnumerable TryFinallyWithTwoExitPointsInNestedCatch(bool b) + { + // The first user IL instruction is already in 2 nested try blocks. + try { + yield return 1; + try { + Console.WriteLine("Nested Try"); + } catch { + if (b) + goto exit1; + else + goto exit2; + } + } finally { + Console.WriteLine("Finally"); + } + exit1: + Console.WriteLine("Exit1"); + yield break; + exit2: + Console.WriteLine("Exit2"); + } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/Util/CollectionExtensions.cs b/ICSharpCode.Decompiler/Util/CollectionExtensions.cs index b4ba4dc54..70aeec44c 100644 --- a/ICSharpCode.Decompiler/Util/CollectionExtensions.cs +++ b/ICSharpCode.Decompiler/Util/CollectionExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace ICSharpCode.Decompiler.Util { @@ -15,6 +16,11 @@ namespace ICSharpCode.Decompiler.Util { return new HashSet(input); } + + public static IEnumerable SkipLast(this IReadOnlyCollection input, int count) + { + return input.Skip(input.Count - count); + } public static T PopOrDefault(this Stack stack) {