diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Async.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Async.cs index f48cabafe..e4657f769 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Async.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Async.cs @@ -34,18 +34,28 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness public async Task Run() { + Console.WriteLine("SimpleBoolTaskMethod:"); await SimpleBoolTaskMethod(); + Console.WriteLine("StreamCopyTo:"); StreamCopyTo(new MemoryStream(new byte[1024]), 16); + Console.WriteLine("StreamCopyToWithConfigureAwait:"); StreamCopyToWithConfigureAwait(new MemoryStream(new byte[1024]), 16); + Console.WriteLine("AwaitInForEach:"); await AwaitInForEach(Enumerable.Range(0, 100).Select(i => Task.FromResult(i))); + Console.WriteLine("TaskMethodWithoutAwaitButWithExceptionHandling:"); await TaskMethodWithoutAwaitButWithExceptionHandling(); #if CS60 + Console.WriteLine($"{nameof(AwaitCatch)}:"); await AwaitCatch(Task.FromResult(1)); + Console.WriteLine($"{nameof(AwaitMultipleCatchBlocks)}:"); await AwaitMultipleCatchBlocks(Task.FromResult(1)); + Console.WriteLine($"{nameof(AwaitMultipleCatchBlocks2)}:"); await AwaitMultipleCatchBlocks2(Task.FromResult(1)); + Console.WriteLine($"{nameof(AwaitInComplexFinally)}:"); Console.WriteLine(await AwaitInComplexFinally()); try { + Console.WriteLine($"{nameof(AwaitFinally)}:"); await AwaitFinally(Task.FromResult(2)); } catch (Exception ex) @@ -53,13 +63,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness Console.WriteLine(ex + " caught!"); } #endif + Console.WriteLine("NestedAwait:"); await NestedAwait(Task.FromResult(Task.FromResult(5))); + Console.WriteLine("AwaitWithStack:"); await AwaitWithStack(Task.FromResult(3)); + Console.WriteLine("AwaitWithStack2:"); await AwaitWithStack2(Task.FromResult(4)); #if CS60 + Console.WriteLine($"{nameof(AwaitInCatch)}:"); await AwaitInCatch(Task.FromResult(1), Task.FromResult(2)); + Console.WriteLine($"{nameof(AwaitInFinally)}:"); await AwaitInFinally(Task.FromResult(2), Task.FromResult(4)); + Console.WriteLine($"{nameof(AwaitInCatchAndFinally)}:"); await AwaitInCatchAndFinally(Task.FromResult(3), Task.FromResult(6), Task.FromResult(9)); + Console.WriteLine($"{nameof(AwaitInFinallyInUsing)}:"); Console.WriteLine(await AwaitInFinallyInUsing(Task.FromResult(new StringWriter()), Task.FromResult(6), Task.FromResult(9))); #endif } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs index 071ff555d..10ae53f5b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs @@ -28,6 +28,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { private int memberField; + private static bool True() + { + return true; + } + public async void SimpleVoidMethod() { Console.WriteLine("Before"); @@ -152,6 +157,155 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } } + + public async Task AnonymousThrow() + { + try + { + await Task.Delay(0); + } + catch + { + await Task.Delay(0); + throw; + } + } + + public async Task DeclaredException() + { + try + { + await Task.Delay(0); + } + catch (Exception) + { + await Task.Delay(0); + throw; + } + } + + public async Task RethrowDeclared() + { + try + { + await Task.Delay(0); + } + catch (Exception ex) + { + await Task.Delay(0); + throw ex; + } + } + + public async Task RethrowDeclaredWithFilter() + { + try + { + await Task.Delay(0); + } + catch (Exception ex) when (ex.GetType().FullName.Contains("asdf")) + { + await Task.Delay(0); + throw; + } + } + + public async Task ComplexCatchBlock() + { + try + { + await Task.Delay(0); + } + catch (Exception ex) + { + if (ex.GetHashCode() != 0) + { + throw; + } + await Task.Delay(0); + } + } + + public async Task ComplexCatchBlockWithFilter() + { + try + { + await Task.Delay(0); + } + catch (Exception ex) when (ex.GetType().FullName.Contains("asdf")) + { + if (ex.GetHashCode() != 0) + { + throw; + } + await Task.Delay(0); + } + } + + public async Task LoadsToCatch(int i) + { + try + { + throw null; + } + catch (Exception ex2) when (i == 0) + { + Console.WriteLine("First!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + Console.WriteLine(ex2.StackTrace); + } + catch (Exception ex3) when (True()) + { + Console.WriteLine("Second!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + Console.WriteLine(ex3.StackTrace); + } + catch (Exception ex) + { + Console.WriteLine("Third!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + Console.WriteLine(ex.StackTrace); + } + catch when (i == 0) + { + Console.WriteLine("Fourth!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + } + catch when (True()) + { + Console.WriteLine("Fifth!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + } + catch + { + Console.WriteLine("Sixth!"); + if (i == 1) + { + throw; + } + await Task.Yield(); + } + } #endif public static async Task GetIntegerSumAsync(IEnumerable items) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncForeach.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncForeach.cs index 2beb02547..b0d46a4be 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncForeach.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncForeach.cs @@ -29,11 +29,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty int sum = 0; await foreach (int item in items.WithCancellation(token)) { - if (token.IsCancellationRequested) + if (!token.IsCancellationRequested) { - break; + sum += item; + continue; } - sum += item; + break; } return sum; } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs index 8cfdfa74f..e4ae2c2eb 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs @@ -448,6 +448,32 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Console.WriteLine("{0} {1}", val, val.ToString()); } } + + public void XXX1() + { + try + { + Console.WriteLine(); + } + catch (Exception ex) when (ex.Data.IsFixedSize) + { + Console.WriteLine(ex.ToString()); + throw ex; + } + } + + public void XXX2() + { + try + { + Console.WriteLine(); + } + catch (Exception ex) when (ex is InternalBufferOverflowException) + { + Console.WriteLine(ex.ToString()); + throw ex; + } + } #endif } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UsingVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UsingVariables.cs index 0d08ce885..911e948c0 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UsingVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UsingVariables.cs @@ -17,7 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; -//using System.Threading.Tasks; +using System.Threading.Tasks; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { @@ -38,7 +38,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } - private void Use(IAsyncDisposable disposable) + private void Use(IAsyncDisposable asyncDisposable) { } @@ -84,12 +84,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Use(disposable2); } - //public async Task SimpleUsingVarAsync(IAsyncDisposable other) - //{ - // Console.WriteLine("before using"); - // await using IAsyncDisposable asyncDisposable = GetAsyncDisposable(); - // Console.WriteLine("inside using"); - // Use(asyncDisposable); - //} + public async Task SimpleUsingVarAsync() + { + Console.WriteLine("before using"); + await using IAsyncDisposable asyncDisposable = GetAsyncDisposable(); + Console.WriteLine("inside using"); + Use(asyncDisposable); + } } } diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs index 63a02b56a..c46fbe74f 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs @@ -18,19 +18,43 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.IL.ControlFlow { class AwaitInCatchTransform { + readonly struct CatchBlockInfo + { + public readonly int Id; + public readonly TryCatchHandler Handler; + public readonly Block RealCatchBlockEntryPoint; + public readonly ILInstruction NextBlockOrExitContainer; + public readonly ILInstruction JumpTableEntry; + public readonly ILVariable ObjectVariable; + + public CatchBlockInfo(int id, TryCatchHandler handler, Block realCatchBlockEntryPoint, + ILInstruction nextBlockOrExitContainer, ILInstruction jumpTableEntry, ILVariable objectVariable) + { + Id = id; + Handler = handler; + RealCatchBlockEntryPoint = realCatchBlockEntryPoint; + NextBlockOrExitContainer = nextBlockOrExitContainer; + JumpTableEntry = jumpTableEntry; + ObjectVariable = objectVariable; + } + } + public static void Run(ILFunction function, ILTransformContext context) { if (!context.Settings.AwaitInCatchFinally) return; HashSet changedContainers = new HashSet(); + HashSet removedBlocks = new HashSet(); // analyze all try-catch statements in the function foreach (var tryCatch in function.Descendants.OfType().ToArray()) @@ -44,28 +68,32 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow changedContainers.Add(container); foreach (var result in transformableCatchBlocks) { + removedBlocks.Clear(); var node = cfg.GetNode(result.RealCatchBlockEntryPoint); - context.StepStartGroup("Inline catch block with await", result.Handler); + context.StepStartGroup($"Inline catch block with await (at {result.Handler.Variable.Name})", result.Handler); // Remove the IfInstruction from the jump table and eliminate all branches to the block. - var jumpTableBlock = (Block)result.JumpTableEntry.Parent; - context.Step("Remove jump-table entry", result.JumpTableEntry); - jumpTableBlock.Instructions.RemoveAt(result.JumpTableEntry.ChildIndex); - - foreach (var branch in tryCatch.Descendants.OfType()) + if (result.JumpTableEntry is IfInstruction jumpTableEntry) { - if (branch.TargetBlock == jumpTableBlock) + var jumpTableBlock = (Block)jumpTableEntry.Parent; + context.Step("Remove jump-table entry", result.JumpTableEntry); + jumpTableBlock.Instructions.RemoveAt(result.JumpTableEntry.ChildIndex); + + foreach (var branch in tryCatch.Descendants.OfType()) { - if (result.NextBlockOrExitContainer is BlockContainer exitContainer) + if (branch.TargetBlock == jumpTableBlock) { - context.Step("branch jumpTableBlock => leave exitContainer", branch); - branch.ReplaceWith(new Leave(exitContainer)); - } - else - { - context.Step("branch jumpTableBlock => branch nextBlock", branch); - branch.ReplaceWith(new Branch((Block)result.NextBlockOrExitContainer)); + if (result.NextBlockOrExitContainer is BlockContainer exitContainer) + { + context.Step("branch jumpTableBlock => leave exitContainer", branch); + branch.ReplaceWith(new Leave(exitContainer)); + } + else + { + context.Step("branch jumpTableBlock => branch nextBlock", branch); + branch.ReplaceWith(new Branch((Block)result.NextBlockOrExitContainer)); + } } } } @@ -79,14 +107,25 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow // Remove the generated catch block catchBlockHead.Remove(); + TransformAsyncThrowToThrow(context, removedBlocks, result.RealCatchBlockEntryPoint); + // Inline all blocks that are dominated by the entrypoint of the real catch block foreach (var n in cfg.cfg) { - if (((Block)n.UserData).Parent == result.Handler.Body) - continue; + Block block = (Block)n.UserData; + if (node.Dominates(n)) { - MoveBlock((Block)n.UserData, (BlockContainer)result.Handler.Body); + TransformAsyncThrowToThrow(context, removedBlocks, block); + + if (block.Parent == result.Handler.Body) + continue; + + if (!removedBlocks.Contains(block)) + { + context.Step("Move block", result.Handler.Body); + MoveBlock(block, (BlockContainer)result.Handler.Body); + } } } @@ -111,14 +150,21 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } // Remove all assignments to the common object variable that stores the exception object. - if (result.ObjectVariableStore != null) + if (result.ObjectVariable != result.Handler.Variable) { - foreach (var load in result.ObjectVariableStore.Variable.LoadInstructions.ToArray()) + foreach (var load in result.ObjectVariable.LoadInstructions.ToArray()) { - if (load.Parent is CastClass cc && cc.Type == result.Handler.Variable.Type) - cc.ReplaceWith(new LdLoc(result.Handler.Variable)); + if (!load.IsDescendantOf(result.Handler)) + continue; + + if (load.Parent is CastClass cc && cc.Type.Equals(result.Handler.Variable.Type)) + { + cc.ReplaceWith(new LdLoc(result.Handler.Variable).WithILRange(cc).WithILRange(load)); + } else - load.ReplaceWith(new LdLoc(result.Handler.Variable)); + { + load.ReplaceWith(new LdLoc(result.Handler.Variable).WithILRange(load)); + } } } @@ -131,6 +177,23 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow container.SortBlocks(deleteUnreachableBlocks: true); } + private static void TransformAsyncThrowToThrow(ILTransformContext context, HashSet removedBlocks, Block block) + { + ILVariable v = null; + if (MatchExceptionCaptureBlock(context, block, + ref v, out StLoc typedExceptionVariableStore, + out Block captureBlock, out Block throwBlock)) + { + context.Step($"ExceptionDispatchInfo.Capture({v.Name}).Throw() => throw;", typedExceptionVariableStore); + block.Instructions.RemoveRange(typedExceptionVariableStore.ChildIndex + 1, 2); + captureBlock.Remove(); + throwBlock.Remove(); + removedBlocks.Add(captureBlock); + removedBlocks.Add(throwBlock); + typedExceptionVariableStore.ReplaceWith(new Rethrow()); + } + } + static void MoveBlock(Block block, BlockContainer target) { block.Remove(); @@ -140,105 +203,282 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow /// /// Analyzes all catch handlers and returns every handler that follows the await catch handler pattern. /// - static bool AnalyzeHandlers(InstructionCollection handlers, out ILVariable catchHandlerIdentifier, out List<(int Id, TryCatchHandler Handler, Block RealCatchBlockEntryPoint, ILInstruction NextBlockOrExitContainer, IfInstruction JumpTableEntry, StLoc ObjectVariableStore)> transformableCatchBlocks) + static bool AnalyzeHandlers(InstructionCollection handlers, out ILVariable catchHandlerIdentifier, + out List transformableCatchBlocks) { - transformableCatchBlocks = new List<(int Id, TryCatchHandler Handler, Block RealCatchBlockEntryPoint, ILInstruction NextBlockOrExitContainer, IfInstruction JumpTableEntry, StLoc ObjectVariableStore)>(); + transformableCatchBlocks = new List(); catchHandlerIdentifier = null; foreach (var handler in handlers) { - if (!MatchAwaitCatchHandler((BlockContainer)handler.Body, out int id, out var identifierVariable, out var realEntryPoint, out var nextBlockOrExitContainer, out var jumpTableEntry, out var objectVariableStore)) + if (!MatchAwaitCatchHandler(handler, out int id, out var identifierVariable, + out var realEntryPoint, out var nextBlockOrExitContainer, out var jumpTableEntry, + out var objectVariable)) + { continue; + } + if (id < 1 || (catchHandlerIdentifier != null && identifierVariable != catchHandlerIdentifier)) + { continue; + } + catchHandlerIdentifier = identifierVariable; - transformableCatchBlocks.Add((id, handler, realEntryPoint, nextBlockOrExitContainer, jumpTableEntry, objectVariableStore)); + transformableCatchBlocks.Add(new(id, handler, realEntryPoint, nextBlockOrExitContainer, jumpTableEntry, objectVariable ?? handler.Variable)); } return transformableCatchBlocks.Count > 0; } /// /// Matches the await catch handler pattern: - /// stloc V_3(ldloc E_100) - copy exception variable to a temporary + /// [stloc V_3(ldloc E_100) - copy exception variable to a temporary] /// stloc V_6(ldloc V_3) - store exception in 'global' object variable /// stloc V_5(ldc.i4 2) - store id of catch block in 'identifierVariable' /// br IL_0075 - jump out of catch block to the head of the catch-handler jump table /// - static bool MatchAwaitCatchHandler(BlockContainer container, out int id, out ILVariable identifierVariable, out Block realEntryPoint, out ILInstruction nextBlockOrExitContainer, out IfInstruction jumpTableEntry, out StLoc objectVariableStore) + static bool MatchAwaitCatchHandler(TryCatchHandler handler, out int id, out ILVariable identifierVariable, + out Block realEntryPoint, out ILInstruction nextBlockOrExitContainer, + out ILInstruction jumpTableEntry, out ILVariable objectVariable) { - id = default(int); + id = 0; identifierVariable = null; realEntryPoint = null; jumpTableEntry = null; - objectVariableStore = null; + objectVariable = null; nextBlockOrExitContainer = null; - var catchBlock = container.EntryPoint; - if (catchBlock.Instructions.Count < 2 || catchBlock.Instructions.Count > 4) - return false; - if (!catchBlock.Instructions.Last().MatchBranch(out var jumpTableStartBlock)) - return false; - if (catchBlock.Instructions.Count > 2 && catchBlock.Instructions[catchBlock.Instructions.Count - 3] is StLoc stloc) + var exceptionVariable = handler.Variable; + var catchBlock = ((BlockContainer)handler.Body).EntryPoint; + ILInstruction value; + switch (catchBlock.Instructions.Count) { - objectVariableStore = stloc; + case 3: + if (!catchBlock.Instructions[0].MatchStLoc(out objectVariable, out value)) + return false; + if (!value.MatchLdLoc(exceptionVariable)) + return false; + break; + case 4: + if (!catchBlock.Instructions[0].MatchStLoc(out var temporaryVariable, out value)) + return false; + if (!value.MatchLdLoc(exceptionVariable)) + return false; + if (!catchBlock.Instructions[1].MatchStLoc(out objectVariable, out value)) + return false; + if (!value.MatchLdLoc(temporaryVariable)) + return false; + break; + default: + // if the exception variable is not used at all (e.g., catch (Exception)) + // the "exception-variable-assignment" is omitted completely. + // This can happen in optimized code. + break; } + if (!catchBlock.Instructions.Last().MatchBranch(out var jumpTableStartBlock)) + return false; var identifierVariableAssignment = catchBlock.Instructions.SecondToLastOrDefault(); - if (!identifierVariableAssignment.MatchStLoc(out identifierVariable, out var value) || !value.MatchLdcI4(out id)) + if (!identifierVariableAssignment.MatchStLoc(out identifierVariable, out value) || !value.MatchLdcI4(out id)) return false; // analyze jump table: - // [stloc identifierVariableCopy(identifierVariable)] - // if (comp(identifierVariable == id)) br realEntryPoint - // br jumpTableEntryBlock - ILVariable identifierVariableCopy; - if (jumpTableStartBlock.Instructions.Count == 3) + switch (jumpTableStartBlock.Instructions.Count) { - if (!jumpTableStartBlock.Instructions[0].MatchStLoc(out identifierVariableCopy, out var identifierVariableLoad) || !identifierVariableLoad.MatchLdLoc(identifierVariable)) + case 3: + // stloc identifierVariableCopy(identifierVariable) + // if (comp(identifierVariable == id)) br realEntryPoint + // br jumpTableEntryBlock + if (!jumpTableStartBlock.Instructions[0].MatchStLoc(out var identifierVariableCopy, out var identifierVariableLoad) + || !identifierVariableLoad.MatchLdLoc(identifierVariable)) + { + return false; + } + return ParseIfJumpTable(id, jumpTableStartBlock, identifierVariableCopy, out realEntryPoint, out nextBlockOrExitContainer, out jumpTableEntry); + case 2: + // if (comp(identifierVariable == id)) br realEntryPoint + // br jumpTableEntryBlock + return ParseIfJumpTable(id, jumpTableStartBlock, identifierVariable, out realEntryPoint, out nextBlockOrExitContainer, out jumpTableEntry); + case 1: + if (jumpTableStartBlock.Instructions[0] is not SwitchInstruction switchInst) + { + return false; + } + + return ParseSwitchJumpTable(id, switchInst, identifierVariable, out realEntryPoint, out nextBlockOrExitContainer, out jumpTableEntry); + default: return false; } - else if (jumpTableStartBlock.Instructions.Count == 2) + + bool ParseSwitchJumpTable(int id, SwitchInstruction jumpTable, ILVariable identifierVariable, out Block realEntryPoint, out ILInstruction nextBlockOrExitContainer, out ILInstruction jumpTableEntry) { - identifierVariableCopy = identifierVariable; - } - else + realEntryPoint = null; + nextBlockOrExitContainer = null; + jumpTableEntry = null; + + if (!jumpTable.Value.MatchLdLoc(identifierVariable)) + return false; + + var defaultSection = jumpTable.GetDefaultSection(); + + foreach (var section in jumpTable.Sections) + { + if (!section.Labels.Contains(id)) + continue; + if (!section.Body.MatchBranch(out realEntryPoint)) + return false; + if (defaultSection.Body.MatchBranch(out var t)) + nextBlockOrExitContainer = t; + else if (defaultSection.Body.MatchLeave(out var t2)) + nextBlockOrExitContainer = t2; + jumpTableEntry = section; + return true; + } + return false; - var jumpTableEntryBlock = jumpTableStartBlock; - do + } + + bool ParseIfJumpTable(int id, Block jumpTableEntryBlock, ILVariable identifierVariable, out Block realEntryPoint, out ILInstruction nextBlockOrExitContainer, out ILInstruction jumpTableEntry) { - if (!(jumpTableEntryBlock.Instructions.SecondToLastOrDefault() is IfInstruction ifInst)) - return false; - ILInstruction lastInst = jumpTableEntryBlock.Instructions.Last(); - if (ifInst.Condition.MatchCompEquals(out var left, out var right)) + realEntryPoint = null; + nextBlockOrExitContainer = null; + jumpTableEntry = null; + do { - if (!ifInst.TrueInst.MatchBranch(out realEntryPoint)) + if (!(jumpTableEntryBlock.Instructions.SecondToLastOrDefault() is IfInstruction ifInst)) return false; - if (!lastInst.MatchBranch(out jumpTableEntryBlock)) + ILInstruction lastInst = jumpTableEntryBlock.Instructions.Last(); + if (ifInst.Condition.MatchCompEquals(out var left, out var right)) { - if (!lastInst.MatchLeave((BlockContainer)lastInst.Parent.Parent)) + if (!ifInst.TrueInst.MatchBranch(out realEntryPoint)) return false; + if (!lastInst.MatchBranch(out jumpTableEntryBlock)) + { + if (!lastInst.MatchLeave((BlockContainer)lastInst.Parent.Parent)) + return false; + } } - } - else if (ifInst.Condition.MatchCompNotEquals(out left, out right)) - { - if (!lastInst.MatchBranch(out realEntryPoint)) - return false; - if (!ifInst.TrueInst.MatchBranch(out jumpTableEntryBlock)) + else if (ifInst.Condition.MatchCompNotEquals(out left, out right)) { - if (!ifInst.TrueInst.MatchLeave((BlockContainer)lastInst.Parent.Parent)) + if (!lastInst.MatchBranch(out realEntryPoint)) return false; + if (!ifInst.TrueInst.MatchBranch(out jumpTableEntryBlock)) + { + if (!ifInst.TrueInst.MatchLeave((BlockContainer)lastInst.Parent.Parent)) + return false; + } } - } - else - { + else + { + return false; + } + if (!left.MatchLdLoc(identifierVariable)) + return false; + if (right.MatchLdcI4(id)) + { + nextBlockOrExitContainer = jumpTableEntryBlock ?? lastInst.Parent.Parent; + jumpTableEntry = ifInst; + return true; + } + } while (jumpTableEntryBlock?.Instructions.Count == 2); + return false; + } + } + + // Block beforeThrowBlock { + // [before throw] + // stloc typedExceptionVariable(isinst System.Exception(ldloc objectVariable)) + // if (comp.o(ldloc typedExceptionVariable != ldnull)) br captureBlock + // br throwBlock + // } + // + // Block throwBlock { + // throw(ldloc objectVariable) + // } + // + // Block captureBlock { + // callvirt Throw(call Capture(ldloc typedExceptionVariable)) + // br nextBlock + // } + // => + // throw(ldloc result.Handler.Variable) + internal static bool MatchExceptionCaptureBlock(ILTransformContext context, Block block, + ref ILVariable objectVariable, out StLoc typedExceptionVariableStore, out Block captureBlock, out Block throwBlock) + { + bool DerivesFromException(IType t) => t.GetAllBaseTypes().Any(ty => ty.IsKnownType(KnownTypeCode.Exception)); + + captureBlock = null; + throwBlock = null; + typedExceptionVariableStore = null; + + var typedExceptionVariableStLoc = block.Instructions.ElementAtOrDefault(block.Instructions.Count - 3) as StLoc; + + if (typedExceptionVariableStLoc == null + || !typedExceptionVariableStLoc.Value.MatchIsInst(out var arg, out var type) + || !DerivesFromException(type) + || !arg.MatchLdLoc(out var v)) + { + return false; + } + + if (objectVariable == null) + { + objectVariable = v; + } + else if (!objectVariable.Equals(v)) + { + return false; + } + + typedExceptionVariableStore = typedExceptionVariableStLoc; + + if (!block.Instructions[block.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst)) + return false; + + ILInstruction lastInstr = block.Instructions.Last(); + if (!lastInstr.MatchBranch(out throwBlock)) + return false; + + if (condition.MatchCompNotEqualsNull(out arg) + && trueInst is Branch branchToCapture) + { + if (!arg.MatchLdLoc(typedExceptionVariableStore.Variable)) return false; - } - if (!left.MatchLdLoc(identifierVariableCopy)) + captureBlock = branchToCapture.TargetBlock; + } + else + { + return false; + } + + if (throwBlock.IncomingEdgeCount != 1 + || throwBlock.Instructions.Count != 1 + || !(throwBlock.Instructions[0].MatchThrow(out var ov) && ov.MatchLdLoc(objectVariable))) + { + return false; + } + + if (captureBlock.IncomingEdgeCount != 1 + || captureBlock.Instructions.Count != 2 + || !MatchCaptureThrowCalls(captureBlock.Instructions[0])) + { + return false; + } + + return true; + + bool MatchCaptureThrowCalls(ILInstruction inst) + { + var exceptionDispatchInfoType = context.TypeSystem.FindType(typeof(System.Runtime.ExceptionServices.ExceptionDispatchInfo)); + if (inst is not CallVirt callVirt || callVirt.Arguments.Count != 1) return false; - if (right.MatchLdcI4(id)) + + if (callVirt.Arguments[0] is not Call call || call.Arguments.Count != 1 + || !call.Arguments[0].MatchLdLoc(typedExceptionVariableStLoc.Variable)) { - nextBlockOrExitContainer = jumpTableEntryBlock ?? lastInst.Parent.Parent; - jumpTableEntry = ifInst; - return true; + return false; } - } while (jumpTableEntryBlock?.Instructions.Count == 2); - return false; + + return callVirt.Method.Name == "Throw" + && callVirt.Method.DeclaringType.Equals(exceptionDispatchInfoType) + && call.Method.Name == "Capture" + && call.Method.DeclaringType.Equals(exceptionDispatchInfoType); + } } } } diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs index 265dda66e..d84dc5ee5 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs @@ -38,218 +38,303 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { if (!(tryCatch.Parent?.Parent is BlockContainer container)) continue; + // } catch exceptionVariable : 02000078 System.Object when (ldc.i4 1) BlockContainer { + // Block IL_004a (incoming: 1) { + // stloc objectVariable(ldloc exceptionVariable) + // br finallyBlock + // } + // + // } + // } + // + // Block finallyBlock (incoming: 2) { + // if (comp.o(ldloc b == ldnull)) br afterFinallyBlock + // br finallyBlockContinuation + // } + // + // Block finallyBlockContinuation (incoming: 1) { + // await(addressof System.Threading.Tasks.ValueTask(callvirt DisposeAsync(ldloc b))) + // br afterFinallyBlock + // } + // + // Block afterFinallyBlock (incoming: 2) { + // stloc V_1(ldloc objectVariable) + // if (comp.o(ldloc V_1 == ldnull)) br IL_00ea + // br IL_00cf + // } + // await in finally uses a single catch block with catch-type object - if (tryCatch.Handlers.Count != 1 || !(tryCatch.Handlers[0].Body is BlockContainer catchBlockContainer) || !tryCatch.Handlers[0].Variable.Type.IsKnownType(KnownTypeCode.Object)) + if (tryCatch.Handlers.Count != 1) + { + continue; + } + var handler = tryCatch.Handlers[0]; + var exceptionVariable = handler.Variable; + if (handler.Body is not BlockContainer catchBlockContainer) continue; - // and consists of an assignment to a temporary that is used outside the catch block - // and a jump to the finally block - var block = catchBlockContainer.EntryPoint; - if (block.Instructions.Count < 2 || !block.Instructions[0].MatchStLoc(out var globalCopyVar, out var value) || !value.MatchLdLoc(tryCatch.Handlers[0].Variable)) + if (!exceptionVariable.Type.IsKnownType(KnownTypeCode.Object)) continue; - if (block.Instructions.Count == 3) + // Matches the await finally pattern: + // [stloc V_3(ldloc E_100) - copy exception variable to a temporary] + // stloc V_6(ldloc V_3) - store exception in 'global' object variable + // br IL_0075 - jump out of catch block to the head of the finallyBlock + var catchBlockEntry = catchBlockContainer.EntryPoint; + ILVariable objectVariable; + switch (catchBlockEntry.Instructions.Count) { - if (!block.Instructions[1].MatchStLoc(out var globalCopyVarTemp, out value) || !value.MatchLdLoc(globalCopyVar)) + case 2: + if (!catchBlockEntry.Instructions[0].MatchStLoc(out objectVariable, out var value)) + continue; + if (!value.MatchLdLoc(exceptionVariable)) + continue; + break; + case 3: + if (!catchBlockEntry.Instructions[0].MatchStLoc(out var temporaryVariable, out value)) + continue; + if (!value.MatchLdLoc(exceptionVariable)) + continue; + if (!catchBlockEntry.Instructions[1].MatchStLoc(out objectVariable, out value)) + continue; + if (!value.MatchLdLoc(temporaryVariable)) + continue; + break; + default: continue; - globalCopyVar = globalCopyVarTemp; } - if (!block.Instructions[block.Instructions.Count - 1].MatchBranch(out var entryPointOfFinally)) + if (!catchBlockEntry.Instructions[catchBlockEntry.Instructions.Count - 1].MatchBranch(out var entryPointOfFinally)) continue; // globalCopyVar should only be used once, at the end of the finally-block - if (globalCopyVar.LoadCount != 1 || globalCopyVar.StoreCount > 2) + if (objectVariable.LoadCount != 1 || objectVariable.StoreCount > 2) continue; - var tempStore = globalCopyVar.LoadInstructions[0].Parent as StLoc; - if (tempStore == null || !MatchExceptionCaptureBlock(tempStore, out var exitOfFinally, out var afterFinally, out var blocksToRemove)) + + var beforeExceptionCaptureBlock = (Block)LocalFunctionDecompiler.GetStatement(objectVariable.LoadInstructions[0])?.Parent; + if (beforeExceptionCaptureBlock == null) continue; - if (!MatchAfterFinallyBlock(ref afterFinally, blocksToRemove, out bool removeFirstInstructionInAfterFinally)) + + var (afterFinallyBlock, capturePatternStart, objectVariableCopy) = FindBlockAfterFinally(context, beforeExceptionCaptureBlock, objectVariable); + if (afterFinallyBlock == null || capturePatternStart == null) + continue; + + var initOfIdentifierVariable = tryCatch.Parent.Children.ElementAtOrDefault(tryCatch.ChildIndex - 1) as StLoc; + if (initOfIdentifierVariable == null || !initOfIdentifierVariable.Value.MatchLdcI4(0)) continue; - var cfg = new ControlFlowGraph(container, context.CancellationToken); - var exitOfFinallyNode = cfg.GetNode(exitOfFinally); - var entryPointOfFinallyNode = cfg.GetNode(entryPointOfFinally); - var additionalBlocksInFinally = new HashSet(); - var invalidExits = new List(); + var identifierVariable = initOfIdentifierVariable.Variable; + + context.StepStartGroup("Inline finally block with await", tryCatch.Handlers[0]); + var cfg = new ControlFlowGraph(container, context.CancellationToken); + changedContainers.Add(container); - TraverseDominatorTree(entryPointOfFinallyNode); + context.StepStartGroup("Move blocks to state assignments"); + Dictionary identifierValueTargets = new Dictionary(); - void TraverseDominatorTree(ControlFlowNode node) + foreach (var load in identifierVariable.LoadInstructions.ToArray()) { - if (entryPointOfFinallyNode != node) + var statement = LocalFunctionDecompiler.GetStatement(load); + var block = (Block)statement.Parent; + + if (!statement.MatchIfInstruction(out var cond, out var branchToTarget)) { - if (entryPointOfFinallyNode.Dominates(node)) - additionalBlocksInFinally.Add((Block)node.UserData); - else - invalidExits.Add(node); + block.Instructions.RemoveAt(statement.ChildIndex); + continue; } - if (node == exitOfFinallyNode) - return; + if (block.Instructions.LastOrDefault() is not Branch otherBranch) + { + block.Instructions.RemoveAt(statement.ChildIndex); + continue; + } - foreach (var child in node.DominatorTreeChildren) + if (cond.MatchCompEquals(out var left, out var right) + && left == load && right.MatchLdcI4(out int value) + && branchToTarget.MatchBranch(out var targetBlock)) { - TraverseDominatorTree(child); + identifierValueTargets.Add(value, targetBlock); + block.Instructions.RemoveAt(statement.ChildIndex); + } + else if (cond.MatchCompNotEquals(out left, out right) + && left == load && right.MatchLdcI4(out value)) + { + identifierValueTargets.Add(value, otherBranch.TargetBlock); + block.Instructions.RemoveAt(statement.ChildIndex + 1); + statement.ReplaceWith(otherBranch); + } + else + { + block.Instructions.RemoveAt(statement.ChildIndex); } } - if (invalidExits.Any()) - continue; + var removedBlocks = new List(); - context.Step("Inline finally block with await", tryCatch.Handlers[0]); - - foreach (var blockToRemove in blocksToRemove) + foreach (var store in identifierVariable.StoreInstructions.OfType().ToArray()) { - blockToRemove.Remove(); + if (!store.Value.MatchLdcI4(out int value)) + continue; + var statement = LocalFunctionDecompiler.GetStatement(store); + var parent = (Block)statement.Parent; + if (value <= 0) + { + parent.Instructions.RemoveAt(statement.ChildIndex); + continue; + } + if (!identifierValueTargets.TryGetValue(value, out var targetBlock)) + { + store.ReplaceWith(new Nop() { Comment = $"Could not find matching block for id {value}" }); + continue; + } + var targetContainer = BlockContainer.FindClosestContainer(statement); + context.Step($"Move block with id={value} {targetBlock.Label} to IL_{store.StartILOffset}", statement); + parent.Instructions.RemoveAt(statement.ChildIndex + 1); + store.ReplaceWith(new Branch(targetBlock)); + + MoveDominatedBlocksToContainer(targetBlock, null, cfg, targetContainer, removedBlocks); } - var finallyContainer = new BlockContainer(); - entryPointOfFinally.Remove(); - if (removeFirstInstructionInAfterFinally) - afterFinally.Instructions.RemoveAt(0); - changedContainers.Add(container); - var outer = BlockContainer.FindClosestContainer(container.Parent); - if (outer != null) - changedContainers.Add(outer); - finallyContainer.Blocks.Add(entryPointOfFinally); - finallyContainer.AddILRange(entryPointOfFinally); - exitOfFinally.Instructions.RemoveRange(tempStore.ChildIndex, 3); - exitOfFinally.Instructions.Add(new Leave(finallyContainer)); - foreach (var branchToFinally in container.Descendants.OfType()) + + + context.StepEndGroup(keepIfEmpty: true); + + var finallyContainer = new BlockContainer().WithILRange(catchBlockContainer); + tryCatch.ReplaceWith(new TryFinally(tryCatch.TryBlock, finallyContainer).WithILRange(tryCatch.TryBlock)); + + context.Step("Move blocks into finally", finallyContainer); + MoveDominatedBlocksToContainer(entryPointOfFinally, beforeExceptionCaptureBlock, cfg, finallyContainer, removedBlocks); + + if (beforeExceptionCaptureBlock.Instructions.Count >= 3) { - if (branchToFinally.TargetBlock == entryPointOfFinally) - branchToFinally.ReplaceWith(new Branch(afterFinally)); + if (beforeExceptionCaptureBlock.Instructions.SecondToLastOrDefault().MatchIfInstruction(out var cond, out var brInst) + && beforeExceptionCaptureBlock.Instructions.LastOrDefault() is Branch branch + && beforeExceptionCaptureBlock.Instructions[beforeExceptionCaptureBlock.Instructions.Count - 3].MatchStLoc(objectVariableCopy, out var value) + && value.MatchLdLoc(objectVariable)) + { + if (cond.MatchCompEqualsNull(out var arg) && arg.MatchLdLoc(objectVariableCopy)) + { + context.Step("Simplify end of finally", beforeExceptionCaptureBlock); + beforeExceptionCaptureBlock.Instructions.RemoveRange(beforeExceptionCaptureBlock.Instructions.Count - 3, 2); + branch.ReplaceWith(new Leave(finallyContainer).WithILRange(branch)); + } + else if (cond.MatchCompNotEqualsNull(out arg) && arg.MatchLdLoc(objectVariableCopy)) + { + context.Step("Simplify end of finally", beforeExceptionCaptureBlock); + beforeExceptionCaptureBlock.Instructions.RemoveRange(beforeExceptionCaptureBlock.Instructions.Count - 3, 2); + branch.ReplaceWith(new Leave(finallyContainer).WithILRange(branch)); + } + } } - foreach (var newBlock in additionalBlocksInFinally) + + foreach (var branch in container.Descendants.OfType()) { - newBlock.Remove(); - finallyContainer.Blocks.Add(newBlock); - finallyContainer.AddILRange(newBlock); + if (branch.TargetBlock == entryPointOfFinally && branch.IsDescendantOf(tryCatch.TryBlock)) + { + context.Step("branch to finally => branch after finally", branch); + branch.ReplaceWith(new Branch(afterFinallyBlock).WithILRange(branch)); + } + else if (branch.TargetBlock == capturePatternStart) + { + if (branch.IsDescendantOf(finallyContainer)) + { + context.Step("branch out of finally container => leave finally container", branch); + branch.ReplaceWith(new Leave(finallyContainer).WithILRange(branch)); + } + } } - tryCatch.ReplaceWith(new TryFinally(tryCatch.TryBlock, finallyContainer).WithILRange(tryCatch.TryBlock)); + + context.StepEndGroup(keepIfEmpty: true); } + context.Step("Clean up", function); + // clean up all modified containers foreach (var container in changedContainers) container.SortBlocks(deleteUnreachableBlocks: true); - } - /// - /// Block finallyHead (incoming: 2) { - /// [body of finally] - /// stloc V_4(ldloc V_1) - /// if (comp(ldloc V_4 == ldnull)) br afterFinally - /// br typeCheckBlock - /// } - /// - /// Block typeCheckBlock (incoming: 1) { - /// stloc S_110(isinst System.Exception(ldloc V_4)) - /// if (comp(ldloc S_110 != ldnull)) br captureBlock - /// br throwBlock - /// } - /// - /// Block throwBlock (incoming: 1) { - /// throw(ldloc V_4) - /// } - /// - /// Block captureBlock (incoming: 1) { - /// callvirt Throw(call Capture(ldloc S_110)) - /// br afterFinally - /// } - /// - /// Block afterFinally (incoming: 2) { - /// stloc V_1(ldnull) - /// [after finally] - /// } - /// - static bool MatchExceptionCaptureBlock(StLoc tempStore, out Block endOfFinally, out Block afterFinally, out List blocksToRemove) - { - afterFinally = null; - endOfFinally = (Block)tempStore.Parent; - blocksToRemove = new List(); - int count = endOfFinally.Instructions.Count; - if (tempStore.ChildIndex != count - 3) - return false; - if (!(endOfFinally.Instructions[count - 2] is IfInstruction ifInst)) - return false; - if (!endOfFinally.Instructions.Last().MatchBranch(out var typeCheckBlock)) - return false; - if (!ifInst.TrueInst.MatchBranch(out afterFinally)) - return false; - // match typeCheckBlock - if (typeCheckBlock.Instructions.Count != 3) - return false; - if (!typeCheckBlock.Instructions[0].MatchStLoc(out var castStore, out var cast) - || !cast.MatchIsInst(out var arg, out var type) || !type.IsKnownType(KnownTypeCode.Exception) || !arg.MatchLdLoc(tempStore.Variable)) - return false; - if (!typeCheckBlock.Instructions[1].MatchIfInstruction(out var cond, out var jumpToCaptureBlock)) - return false; - if (!cond.MatchCompNotEqualsNull(out arg) || !arg.MatchLdLoc(castStore)) - return false; - if (!typeCheckBlock.Instructions[2].MatchBranch(out var throwBlock)) - return false; - if (!jumpToCaptureBlock.MatchBranch(out var captureBlock)) - return false; - // match throwBlock - if (throwBlock.Instructions.Count != 1 || !throwBlock.Instructions[0].MatchThrow(out arg) || !arg.MatchLdLoc(tempStore.Variable)) - return false; - // match captureBlock - if (captureBlock.Instructions.Count != 2) - return false; - if (!captureBlock.Instructions[1].MatchBranch(afterFinally)) - return false; - if (!(captureBlock.Instructions[0] is CallVirt callVirt) || callVirt.Method.FullName != "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw" || callVirt.Arguments.Count != 1) - return false; - if (!(callVirt.Arguments[0] is Call call) || call.Method.FullName != "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture" || call.Arguments.Count != 1) - return false; - if (!call.Arguments[0].MatchLdLoc(castStore)) - return false; - blocksToRemove.Add(typeCheckBlock); - blocksToRemove.Add(throwBlock); - blocksToRemove.Add(captureBlock); - return true; - } + ((BlockContainer)function.Body).SortBlocks(deleteUnreachableBlocks: true); - static bool MatchAfterFinallyBlock(ref Block afterFinally, List blocksToRemove, out bool removeFirstInstructionInAfterFinally) - { - removeFirstInstructionInAfterFinally = false; - if (afterFinally.Instructions.Count < 2) - return false; - ILVariable globalCopyVarSplitted; - switch (afterFinally.Instructions[0]) + void MoveDominatedBlocksToContainer(Block newEntryPoint, Block endBlock, ControlFlowGraph graph, + BlockContainer targetContainer, List removedBlocks) { - case IfInstruction ifInst: - if (ifInst.Condition.MatchCompEquals(out var load, out var ldone) && ldone.MatchLdcI4(1) && load.MatchLdLoc(out var variable)) - { - if (!ifInst.TrueInst.MatchBranch(out var targetBlock)) - return false; - blocksToRemove.Add(afterFinally); - afterFinally = targetBlock; - return true; - } - else if (ifInst.Condition.MatchCompNotEquals(out load, out ldone) && ldone.MatchLdcI4(1) && load.MatchLdLoc(out variable)) + var node = graph.GetNode(newEntryPoint); + var endNode = endBlock == null ? null : graph.GetNode(endBlock); + + MoveBlock(newEntryPoint, targetContainer); + + foreach (var n in graph.cfg) + { + Block block = (Block)n.UserData; + + if (node.Dominates(n)) { - if (!afterFinally.Instructions[1].MatchBranch(out var targetBlock)) - return false; - blocksToRemove.Add(afterFinally); - afterFinally = targetBlock; - return true; + if (endNode != null && endNode != n && endNode.Dominates(n)) + continue; + + if (block.Parent == targetContainer) + continue; + + if (!removedBlocks.Contains(block)) + { + MoveBlock(block, targetContainer); + } } - return false; - case LdLoc ldLoc: - if (ldLoc.Variable.LoadCount != 1 || ldLoc.Variable.StoreCount != 1) - return false; - if (!afterFinally.Instructions[1].MatchStLoc(out globalCopyVarSplitted, out var ldnull) || !ldnull.MatchLdNull()) - return false; - removeFirstInstructionInAfterFinally = true; - break; - case StLoc stloc: - globalCopyVarSplitted = stloc.Variable; - if (!stloc.Value.MatchLdNull()) - return false; - break; - default: - return false; + } + } + + void MoveBlock(Block block, BlockContainer target) + { + context.Step($"Move {block.Label} to container at IL_{target.StartILOffset:x4}", target); + block.Remove(); + target.Blocks.Add(block); } - if (globalCopyVarSplitted.StoreCount != 1 || globalCopyVarSplitted.LoadCount != 0) - return false; - return true; + } + + static (Block, Block, ILVariable) FindBlockAfterFinally(ILTransformContext context, Block block, ILVariable objectVariable) + { + int count = block.Instructions.Count; + if (count < 3) + return default; + + if (!block.Instructions[count - 3].MatchStLoc(out var objectVariableCopy, out var value)) + return default; + + if (!value.MatchLdLoc(objectVariable)) + return default; + + if (!block.Instructions[count - 2].MatchIfInstruction(out var cond, out var afterExceptionCaptureBlockBranch)) + return default; + + if (!afterExceptionCaptureBlockBranch.MatchBranch(out var afterExceptionCaptureBlock)) + return default; + + if (!block.Instructions[count - 1].MatchBranch(out var exceptionCaptureBlock)) + return default; + + if (cond.MatchCompEqualsNull(out var arg)) + { + if (!arg.MatchLdLoc(objectVariableCopy)) + return default; + } + else if (cond.MatchCompNotEqualsNull(out arg)) + { + if (!arg.MatchLdLoc(objectVariableCopy)) + return default; + (afterExceptionCaptureBlock, exceptionCaptureBlock) = (exceptionCaptureBlock, afterExceptionCaptureBlock); + } + else + { + return default; + } + + if (!AwaitInCatchTransform.MatchExceptionCaptureBlock(context, exceptionCaptureBlock, + ref objectVariableCopy, out var store, out _, out _)) + { + return default; + } + + var exceptionCaptureBlockStart = LocalFunctionDecompiler.GetStatement(store); + if (exceptionCaptureBlockStart == null) + return default; + + + return (afterExceptionCaptureBlock, (Block)exceptionCaptureBlockStart.Parent, objectVariableCopy); } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs index 91aeefc81..f68c6d076 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs @@ -17,6 +17,8 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.TypeSystem; @@ -52,6 +54,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms // stloc temp(isinst exceptionType(ldloc exceptionVar)) // if (comp(ldloc temp != ldnull)) br whenConditionBlock // br falseBlock + context.Step($"Detected catch-when for {catchBlock.Variable.Name} (extra store)", instructions[0]); ((StLoc)instructions[0]).Value = exceptionSlot; instructions[1].ReplaceWith(new Branch(whenConditionBlock)); instructions.RemoveAt(2); @@ -61,12 +64,95 @@ namespace ICSharpCode.Decompiler.IL.Transforms { // if (comp(isinst exceptionType(ldloc exceptionVar) != ldnull)) br whenConditionBlock // br falseBlock + context.Step($"Detected catch-when for {catchBlock.Variable.Name}", instructions[0]); instructions[0].ReplaceWith(new Branch(whenConditionBlock)); instructions.RemoveAt(1); container.SortBlocks(deleteUnreachableBlocks: true); } + + PropagateExceptionVariable(context, catchBlock); + } + } + } + + /// + /// catch E_189 : 0200007C System.Exception when (BlockContainer { + /// Block IL_0079 (incoming: 1) { + /// stloc S_30(ldloc E_189) + /// br IL_0085 + /// } + /// + /// Block IL_0085 (incoming: 1) { + /// stloc I_1(ldloc S_30) + /// where S_30 and I_1 are single definition + /// => + /// copy-propagate E_189 to replace all uses of S_30 and I_1 + /// + static void PropagateExceptionVariable(ILTransformContext context, TryCatchHandler handler) + { + var exceptionVariable = handler.Variable; + if (!exceptionVariable.IsSingleDefinition) + return; + context.StepStartGroup(nameof(PropagateExceptionVariable)); + int i = 0; + while (i < exceptionVariable.LoadInstructions.Count) + { + var load = exceptionVariable.LoadInstructions[i]; + + if (!load.IsDescendantOf(handler)) + { + i++; + continue; + } + + // We are only interested in store "statements" copying the exception variable + // without modifying it. + var statement = LocalFunctionDecompiler.GetStatement(load); + if (!(statement is StLoc stloc)) + { + i++; + continue; + } + // simple copy case: + // stloc b(ldloc a) + if (stloc.Value == load) + { + PropagateExceptionInstance(stloc); + } + // if the type of the cast-class instruction matches the exceptionType, + // this cast can be removed without losing any side-effects. + // Note: this would also hold true iff exceptionType were an exception derived + // from cc.Type, however, we are more restrictive to match the pattern exactly. + // stloc b(castclass exceptionType(ldloc a)) + else if (stloc.Value is CastClass cc && cc.Type.Equals(exceptionVariable.Type) && cc.Argument == load) + { + stloc.Value = load; + PropagateExceptionInstance(stloc); + } + else + { + i++; + } + + void PropagateExceptionInstance(StLoc store) + { + foreach (var load in store.Variable.LoadInstructions.ToArray()) + { + if (!load.IsDescendantOf(handler)) + continue; + load.ReplaceWith(new LdLoc(exceptionVariable).WithILRange(load)); + } + if (store.Variable.LoadCount == 0 && store.Parent is Block block) + { + block.Instructions.RemoveAt(store.ChildIndex); + } + else + { + i++; + } } } + context.StepEndGroup(keepIfEmpty: false); } /// diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 663d4a83d..827b902a3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -892,7 +892,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms void TransformCatchVariable(TryCatchHandler handler, Block entryPoint, bool isCatchBlock) { if (!handler.Variable.IsSingleDefinition || handler.Variable.LoadCount != 1) - return; // handle.Variable already has non-trivial uses + return; // handler.Variable already has non-trivial uses if (!entryPoint.Instructions[0].MatchStLoc(out var exceptionVar, out var exceptionSlotLoad)) { // Not the pattern with a second exceptionVar.