Browse Source

Merge branch 'PR1258'

pull/1296/head
Daniel Grunwald 7 years ago
parent
commit
cfcb73f908
  1. 5
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 665
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.cs
  3. 2186
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.il
  4. 1479
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.opt.il
  5. 1533
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.opt.roslyn.il
  6. 2343
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.roslyn.il
  7. 27
      ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs
  8. 87
      ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs
  9. 34
      ICSharpCode.Decompiler/IL/ControlFlow/SwitchAnalysis.cs
  10. 356
      ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs
  11. 61
      ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs
  12. 2
      ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs
  13. 47
      ICSharpCode.Decompiler/Util/CollectionExtensions.cs
  14. 3
      ICSharpCode.Decompiler/Util/LongSet.cs

5
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -121,7 +121,10 @@ namespace ICSharpCode.Decompiler.Tests
[Test] [Test]
public void Switch([ValueSource("defaultOptions")] CSharpCompilerOptions cscOptions) public void Switch([ValueSource("defaultOptions")] CSharpCompilerOptions cscOptions)
{ {
RunForLibrary(cscOptions: cscOptions); RunForLibrary(cscOptions: cscOptions, decompilerSettings: new DecompilerSettings {
// legacy csc generates a dead store in debug builds
RemoveDeadCode = (cscOptions == CSharpCompilerOptions.None)
});
} }
[Test] [Test]

665
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.cs

@ -18,6 +18,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
@ -105,6 +106,40 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
} }
public static void SparseIntegerSwitch2(int i)
{
switch (i) {
case 4:
case 10:
case 11:
case 13:
case 21:
case 29:
case 33:
case 49:
case 50:
case 55:
Console.WriteLine();
break;
}
}
public static bool SparseIntegerSwitch3(int i)
{
switch (i) {
case 0:
case 10:
case 11:
case 12:
case 100:
case 101:
case 200:
return true;
default:
return false;
}
}
public static string SwitchOverNullableInt(int? i) public static string SwitchOverNullableInt(int? i)
{ {
switch (i) { switch (i) {
@ -222,6 +257,25 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
} }
// SwitchDetection.UseCSharpSwitch requires more complex heuristic to identify this when compiled with Roslyn
public static void CompactSwitchOverInt(int i)
{
switch (i) {
case 0:
case 1:
case 2:
Console.WriteLine("012");
break;
case 3:
Console.WriteLine("3");
break;
default:
Console.WriteLine("default");
break;
}
Console.WriteLine("end");
}
public static string ShortSwitchOverString(string text) public static string ShortSwitchOverString(string text)
{ {
Console.WriteLine("ShortSwitchOverString: " + text); Console.WriteLine("ShortSwitchOverString: " + text);
@ -369,6 +423,85 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine("End of method"); Console.WriteLine("End of method");
} }
// Needs to be long enough to generate a hashtable
public static void SwitchWithGotoString(string s)
{
Console.WriteLine("SwitchWithGotoString: " + s);
switch (s) {
case "1":
Console.WriteLine("one");
goto default;
case "2":
Console.WriteLine("two");
goto case "3";
case "3":
Console.WriteLine("three");
break;
case "4":
Console.WriteLine("four");
return;
case "5":
Console.WriteLine("five");
return;
case "6":
Console.WriteLine("six");
return;
case "7":
Console.WriteLine("seven");
return;
case "8":
Console.WriteLine("eight");
return;
case "9":
Console.WriteLine("nine");
return;
default:
Console.WriteLine("default");
break;
}
Console.WriteLine("End of method");
}
public static void SwitchWithGotoComplex(string s)
{
Console.WriteLine("SwitchWithGotoComplex: " + s);
switch (s) {
case "1":
Console.WriteLine("one");
goto case "8";
case "2":
Console.WriteLine("two");
goto case "3";
case "3":
Console.WriteLine("three");
if (s.Length != 2) {
break;
}
goto case "5";
case "4":
Console.WriteLine("four");
goto case "5";
case "5":
Console.WriteLine("five");
goto case "8";
case "6":
Console.WriteLine("six");
goto case "5";
case "8":
Console.WriteLine("eight");
return;
// add a default case so that case "7": isn't redundant
default:
Console.WriteLine("default");
break;
// note that goto case "7" will decompile as break;
// cases with a single break have the highest IL offset and are moved to the bottom
case "7":
break;
}
Console.WriteLine("End of method");
}
private static SetProperty[] GetProperties() private static SetProperty[] GetProperties()
{ {
return new SetProperty[0]; return new SetProperty[0];
@ -447,5 +580,537 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
Console.WriteLine("end"); Console.WriteLine("end");
} }
public static void SwitchWithContinue1(int i, bool b)
{
while (true) {
switch (i) {
#if OPT
case 1:
continue;
#endif
case 0:
if (b) {
continue;
}
break;
case 2:
if (!b) {
continue;
}
break;
#if !OPT
case 1:
continue;
#endif
}
Console.WriteLine();
}
}
// while condition, return and break cases
public static void SwitchWithContinue2(int i, bool b)
{
while (i < 10) {
switch (i) {
case 0:
if (b) {
Console.WriteLine("0b");
continue;
}
Console.WriteLine("0!b");
break;
case 2:
#if OPT
if (b) {
Console.WriteLine("2b");
return;
}
Console.WriteLine("2!b");
continue;
#else
if (!b) {
Console.WriteLine("2!b");
continue;
}
Console.WriteLine("2b");
return;
#endif
default:
Console.WriteLine("default");
break;
case 3:
break;
case 1:
continue;
}
Console.WriteLine("loop-tail");
i++;
}
}
// for loop version
public static void SwitchWithContinue3(bool b)
{
for (int i = 0; i < 10; i++) {
switch (i) {
case 0:
if (b) {
Console.WriteLine("0b");
continue;
}
Console.WriteLine("0!b");
break;
case 2:
#if OPT
if (b) {
Console.WriteLine("2b");
return;
}
Console.WriteLine("2!b");
continue;
#else
if (!b) {
Console.WriteLine("2!b");
continue;
}
Console.WriteLine("2b");
return;
#endif
default:
Console.WriteLine("default");
break;
case 3:
break;
case 1:
continue;
}
Console.WriteLine("loop-tail");
}
}
// foreach version
public static void SwitchWithContinue4(bool b)
{
foreach (int item in Enumerable.Range(0, 10)) {
Console.WriteLine("loop: " + item);
switch (item) {
case 1:
if (b) {
continue;
}
break;
case 3:
if (!b) {
continue;
}
return;
case 4:
Console.WriteLine(4);
goto case 7;
case 5:
Console.WriteLine(5);
goto default;
case 6:
if (b) {
continue;
}
goto case 3;
case 7:
if (item % 2 == 0) {
goto case 3;
}
if (!b) {
continue;
}
goto case 8;
case 8:
if (b) {
continue;
}
goto case 5;
default:
Console.WriteLine("default");
break;
case 2:
continue;
}
Console.WriteLine("break: " + item);
}
}
// internal if statement, loop increment block not dominated by the switch head
public static void SwitchWithContinue5(bool b)
{
for (int i = 0; i < 10; i++) {
if (i < 5) {
switch (i) {
case 0:
if (b) {
Console.WriteLine("0b");
continue;
}
Console.WriteLine("0!b");
break;
case 2:
#if OPT
if (b) {
Console.WriteLine("2b");
return;
}
Console.WriteLine("2!b");
continue;
#else
if (!b) {
Console.WriteLine("2!b");
continue;
}
Console.WriteLine("2b");
return;
#endif
default:
Console.WriteLine("default");
break;
case 3:
break;
case 1:
continue;
}
Console.WriteLine("break-target");
}
Console.WriteLine("loop-tail");
}
}
// do-while loop version
public static void SwitchWithContinue6(int i, bool b)
{
do {
switch (i) {
case 0:
if (!b) {
Console.WriteLine("0!b");
break;
}
Console.WriteLine("0b");
// ConditionDetection doesn't recognise Do-While continues yet
continue;
case 2:
if (b) {
Console.WriteLine("2b");
return;
}
Console.WriteLine("2!b");
continue;
default:
Console.WriteLine("default");
break;
case 3:
break;
case 1:
continue;
}
Console.WriteLine("loop-tail");
} while (++i < 10);
}
// double break from switch to loop exit requires additional pattern matching in HighLevelLoopTransform
public static void SwitchWithContinue7()
{
for (int num = 0; num >= 0; num--) {
Console.WriteLine("loop-head");
switch (num) {
default:
Console.WriteLine("default");
break;
case 0:
continue;
case 1:
break;
}
break;
}
Console.WriteLine("end");
}
public static void SwitchWithContinueInDoubleLoop()
{
bool value = false;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
switch (i + j) {
case 1:
case 3:
case 5:
case 7:
case 11:
case 13:
case 17:
break;
default:
continue;
}
value = true;
break;
}
}
Console.WriteLine(value);
}
public static void SwitchLoopNesting()
{
for (int i = 0; i < 10; i++) {
switch (i) {
case 0:
Console.WriteLine(0);
break;
case 1:
Console.WriteLine(1);
break;
default:
if (i % 2 == 0) {
while (i % 3 != 0) {
Console.WriteLine(i++);
}
}
Console.WriteLine();
break;
}
if (i > 4) {
Console.WriteLine("high");
} else {
Console.WriteLine("low");
}
}
}
// These decompile poorly into switch statements and should be left as is
#region Overagressive Switch Use
#if ROSLYN || OPT
public static void SingleIf1(int i, bool a)
{
if (i == 1 || (i == 2 && a)) {
Console.WriteLine(1);
}
Console.WriteLine(2);
}
#endif
public static void SingleIf2(int i, bool a, bool b)
{
if (i == 1 || (i == 2 && a) || (i == 3 && b)) {
Console.WriteLine(1);
}
Console.WriteLine(2);
}
public static void SingleIf3(int i, bool a, bool b)
{
if (a || i == 1 || (i == 2 && b)) {
Console.WriteLine(1);
}
Console.WriteLine(2);
}
public static void SingleIf4(int i, bool a)
{
if (i == 1 || i == 2 || (i != 3 && a) || i != 4) {
Console.WriteLine(1);
}
Console.WriteLine(2);
}
public static void NestedIf(int i)
{
if (i != 1) {
if (i == 2) {
Console.WriteLine(2);
}
Console.WriteLine("default");
}
Console.WriteLine();
}
public static void IfChainWithCondition(int i)
{
if (i == 0) {
Console.WriteLine(0);
} else if (i == 1) {
Console.WriteLine(1);
} else if (i == 2) {
Console.WriteLine(2);
} else if (i == 3) {
Console.WriteLine(3);
} else if (i == 4) {
Console.WriteLine(4);
} else if (i == 5 && Console.CapsLock) {
Console.WriteLine("5A");
} else {
Console.WriteLine("default");
}
Console.WriteLine();
}
public static bool SwitchlikeIf(int i, int j)
{
if (i != 0 && j != 0) {
if (i == -1 && j == -1) {
Console.WriteLine("-1, -1");
}
if (i == -1 && j == 1) {
Console.WriteLine("-1, 1");
}
if (i == 1 && j == -1) {
Console.WriteLine("1, -1");
}
if (i == 1 && j == 1) {
Console.WriteLine("1, 1");
}
return false;
}
if (i != 0) {
if (i == -1) {
Console.WriteLine("-1, 0");
}
if (i == 1) {
Console.WriteLine("1, 0");
}
return false;
}
if (j != 0) {
if (j == -1) {
Console.WriteLine("0, -1");
}
if (j == 1) {
Console.WriteLine("0, 1");
}
return false;
}
return true;
}
public static bool SwitchlikeIf2(int i)
{
if (i != 0) {
// note that using else-if in this chain creates a nice-looking switch here (as expected)
if (i == 1) {
Console.WriteLine(1);
}
if (i == 2) {
Console.WriteLine(2);
}
if (i == 3) {
Console.WriteLine(3);
}
return false;
}
return false;
}
public static void SingleIntervalIf(char c)
{
if (c >= 'A' && c <= 'Z') {
Console.WriteLine("alphabet");
}
Console.WriteLine("end");
}
public static bool Loop8(char c, bool b, Func<char> getChar)
{
if (b) {
while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
c = getChar();
}
}
return true;
}
public static void Loop9(Func<char> getChar)
{
char c;
do {
c = getChar();
} while (c != -1 && c != '\n' && c != '\u2028' && c != '\u2029');
}
#endregion
// Ensure correctness of SwitchDetection.UseCSharpSwitch control flow heuristics
public static void SwitchWithBreakCase(int i, bool b)
{
if (b) {
switch (i) {
case 1:
Console.WriteLine(1);
break;
default:
Console.WriteLine("default");
break;
case 2:
break;
}
Console.WriteLine("b");
}
Console.WriteLine("end");
}
public static void SwitchWithReturnAndBreak(int i, bool b)
{
switch (i) {
case 0:
if (b) {
return;
}
break;
case 1:
if (!b) {
return;
}
break;
}
Console.WriteLine();
}
public static int SwitchWithReturnAndBreak2(int i, bool b)
{
switch (i) {
case 4:
case 33:
Console.WriteLine();
return 1;
case 334:
if (b) {
return 2;
}
break;
case 395:
case 410:
case 455:
Console.WriteLine();
break;
}
Console.WriteLine();
return 0;
}
public static void SwitchWithReturnAndBreak3(int i)
{
switch (i) {
default:
return;
case 0:
Console.WriteLine(0);
break;
case 1:
Console.WriteLine(1);
break;
}
Console.WriteLine();
}
} }
} }

2186
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.il

File diff suppressed because it is too large Load Diff

1479
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.opt.il

File diff suppressed because it is too large Load Diff

1533
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.opt.roslyn.il

File diff suppressed because it is too large Load Diff

2343
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Switch.roslyn.il

File diff suppressed because it is too large Load Diff

27
ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs

@ -47,7 +47,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
private BlockTransformContext context; private BlockTransformContext context;
private ControlFlowNode cfgNode; private ControlFlowNode cfgNode;
private BlockContainer currentContainer; private BlockContainer currentContainer;
private Block continueBlock;
/// <summary> /// <summary>
/// Builds structured control flow for the block associated with the control flow node. /// Builds structured control flow for the block associated with the control flow node.
@ -61,9 +60,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
this.context = context; this.context = context;
currentContainer = (BlockContainer)block.Parent; currentContainer = (BlockContainer)block.Parent;
// for detection of continue statements
continueBlock = GuessContinueBlock();
// We only embed blocks into this block if they aren't referenced anywhere else, // We only embed blocks into this block if they aren't referenced anywhere else,
// so those blocks are dominated by this block. // so those blocks are dominated by this block.
// BlockILTransform thus guarantees that the blocks being embedded are already // BlockILTransform thus guarantees that the blocks being embedded are already
@ -339,6 +335,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
&& ThenInstIsSingleExit(elseIfInst); && ThenInstIsSingleExit(elseIfInst);
} }
private void InvertIf(Block block, IfInstruction ifInst) => InvertIf(block, ifInst, context);
/// <summary> /// <summary>
/// if (cond) { then... } /// if (cond) { then... }
/// else...; /// else...;
@ -349,7 +347,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// ///
/// Assumes ifInst does not have an else block /// Assumes ifInst does not have an else block
/// </summary> /// </summary>
private void InvertIf(Block block, IfInstruction ifInst) internal static void InvertIf(Block block, IfInstruction ifInst, ILTransformContext context)
{ {
Debug.Assert(ifInst.Parent == block); Debug.Assert(ifInst.Parent == block);
@ -488,7 +486,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// breaks have highest priority in a switch // breaks have highest priority in a switch
if ((keyword1 == Keyword.Break) != (keyword2 == Keyword.Break)) if ((keyword1 == Keyword.Break) != (keyword2 == Keyword.Break))
return keyword1 == Keyword.Break ? 1 : -1; return keyword1 == Keyword.Break ? 1 : -1;
} else { } else {
// breaks have lowest priority // breaks have lowest priority
if ((keyword1 == Keyword.Break) != (keyword2 == Keyword.Break)) if ((keyword1 == Keyword.Break) != (keyword2 == Keyword.Break))
@ -537,7 +534,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
keyword = Keyword.Other; keyword = Keyword.Other;
switch (exitInst) { switch (exitInst) {
case Branch branch: case Branch branch:
if (branch.TargetBlock == continueBlock) { if (IsContinueBlock(branch.TargetContainer, branch.TargetBlock)) {
keyword = Keyword.Continue; keyword = Keyword.Continue;
return true; return true;
} }
@ -599,19 +596,19 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// Used to identify branches targetting this block as continue statements, for ordering priority. /// Used to identify branches targetting this block as continue statements, for ordering priority.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private Block GuessContinueBlock() private static bool IsContinueBlock(BlockContainer container, Block block)
{ {
if (currentContainer.Kind != ContainerKind.Loop) if (container.Kind != ContainerKind.Loop)
return null; return false;
// continue blocks have exactly 2 incoming edges // increment blocks have exactly 2 incoming edges
if (currentContainer.EntryPoint.IncomingEdgeCount == 2) { if (container.EntryPoint.IncomingEdgeCount == 2) {
var forIncrement = HighLevelLoopTransform.GetIncrementBlock(currentContainer, currentContainer.EntryPoint); var forIncrement = HighLevelLoopTransform.GetIncrementBlock(container, container.EntryPoint);
if (forIncrement != null) if (forIncrement != null)
return forIncrement; return block == forIncrement;
} }
return currentContainer.EntryPoint; return block == container.EntryPoint;
} }
/// <summary> /// <summary>

87
ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs

@ -42,6 +42,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// <summary>Block container corresponding to the current cfg.</summary> /// <summary>Block container corresponding to the current cfg.</summary>
BlockContainer currentBlockContainer; BlockContainer currentBlockContainer;
/// <summary>
/// Enabled during DetectSwitchBody, used by ExtendLoop and children
/// </summary>
private bool isSwitch;
/// <summary>
/// Used when isSwitch == true, to determine appropriate exit points within loops
/// </summary>
private SwitchDetection.LoopContext loopContext;
/// <summary> /// <summary>
/// Check whether 'block' is a loop head; and construct a loop instruction /// Check whether 'block' is a loop head; and construct a loop instruction
@ -234,17 +243,17 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// ///
/// Requires and maintains the invariant that a node is marked as visited iff it is contained in the loop. /// Requires and maintains the invariant that a node is marked as visited iff it is contained in the loop.
/// </remarks> /// </remarks>
void ExtendLoop(ControlFlowNode loopHead, List<ControlFlowNode> loop, out ControlFlowNode exitPoint, bool isSwitch=false) void ExtendLoop(ControlFlowNode loopHead, List<ControlFlowNode> loop, out ControlFlowNode exitPoint)
{ {
exitPoint = FindExitPoint(loopHead, loop, isSwitch); exitPoint = FindExitPoint(loopHead, loop);
Debug.Assert(!loop.Contains(exitPoint), "Cannot pick an exit point that is part of the natural loop"); Debug.Assert(!loop.Contains(exitPoint), "Cannot pick an exit point that is part of the natural loop");
if (exitPoint != null) { if (exitPoint != null) {
// Either we are in case 1 and just picked an exit that maximizes the amount of code // Either we are in case 1 and just picked an exit that maximizes the amount of code
// outside the loop, or we are in case 2 and found an exit point via post-dominance. // outside the loop, or we are in case 2 and found an exit point via post-dominance.
// Note that if exitPoint == NoExitPoint, we end up adding all dominated blocks to the loop. // Note that if exitPoint == NoExitPoint, we end up adding all dominated blocks to the loop.
var ep = exitPoint; var ep = exitPoint;
foreach (var node in TreeTraversal.PreOrder(loopHead, n => (n != ep) ? n.DominatorTreeChildren : null)) { foreach (var node in TreeTraversal.PreOrder(loopHead, n => DominatorTreeChildren(n, ep))) {
if (node != exitPoint && !node.Visited) { if (!node.Visited) {
node.Visited = true; node.Visited = true;
loop.Add(node); loop.Add(node);
} }
@ -272,14 +281,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// 3) otherwise (exit point unknown, heuristically extend loop): null /// 3) otherwise (exit point unknown, heuristically extend loop): null
/// </returns> /// </returns>
/// <remarks>This method must not write to the Visited flags on the CFG.</remarks> /// <remarks>This method must not write to the Visited flags on the CFG.</remarks>
ControlFlowNode FindExitPoint(ControlFlowNode loopHead, IReadOnlyList<ControlFlowNode> naturalLoop, bool treatBackEdgesAsExits) internal ControlFlowNode FindExitPoint(ControlFlowNode loopHead, IReadOnlyList<ControlFlowNode> naturalLoop)
{ {
bool hasReachableExit = context.ControlFlowGraph.HasReachableExit(loopHead); bool hasReachableExit = HasReachableExit(loopHead);
if (!hasReachableExit && treatBackEdgesAsExits) {
// If we're analyzing the switch, there's no reachable exit, but the loopHead (=switchHead) block
// is also a loop head, we consider the back-edge a reachable exit for the switch.
hasReachableExit = loopHead.Predecessors.Any(p => loopHead.Dominates(p));
}
if (!hasReachableExit) { if (!hasReachableExit) {
// Case 1: // Case 1:
// There are no nodes n so that loopHead dominates a predecessor of n but not n itself // There are no nodes n so that loopHead dominates a predecessor of n but not n itself
@ -305,7 +309,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// We need to pick our exit point so that all paths from the loop head // We need to pick our exit point so that all paths from the loop head
// to the reachable exits run through that exit point. // to the reachable exits run through that exit point.
var cfg = context.ControlFlowGraph.cfg; var cfg = context.ControlFlowGraph.cfg;
var revCfg = PrepareReverseCFG(loopHead, treatBackEdgesAsExits, out int exitNodeArity); var revCfg = PrepareReverseCFG(loopHead, out int exitNodeArity);
//ControlFlowNode.ExportGraph(cfg).Show("cfg"); //ControlFlowNode.ExportGraph(cfg).Show("cfg");
//ControlFlowNode.ExportGraph(revCfg).Show("rev"); //ControlFlowNode.ExportGraph(revCfg).Show("rev");
ControlFlowNode commonAncestor = revCfg[loopHead.UserIndex]; ControlFlowNode commonAncestor = revCfg[loopHead.UserIndex];
@ -338,13 +342,17 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// * The loop has a single exit point that wasn't considered during post-dominance analysis. // * The loop has a single exit point that wasn't considered during post-dominance analysis.
// (which means the single exit isn't dominated by the loop head) // (which means the single exit isn't dominated by the loop head)
// -> we should return NoExitPoint so that all code dominated by the loop head is included into the loop // -> we should return NoExitPoint so that all code dominated by the loop head is included into the loop
if (exitNodeArity > 1) { if (exitNodeArity > 1)
return null; return null;
} else {
// If exitNodeArity == 0, we should maybe look test if our exits out of the block container are all compatible? // Exit node is on the very edge of the tree, and isn't important for determining inclusion
// but I don't think it hurts to have a bit too much code inside the loop in this rare case. // Still necessary for switch detection to insert correct leave statements
return NoExitPoint; if (exitNodeArity == 1 && isSwitch)
} return loopContext.GetBreakTargets(loopHead).Distinct().Single();
// If exitNodeArity == 0, we should maybe look test if our exits out of the block container are all compatible?
// but I don't think it hurts to have a bit too much code inside the loop in this rare case.
return NoExitPoint;
} }
} }
@ -385,6 +393,22 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
} }
} }
/// <summary>
/// Extension of ControlFlowGraph.HasReachableExit
/// Uses loopContext.GetBreakTargets().Any() when analyzing switches to avoid
/// classifying continue blocks as reachable exits.
/// </summary>
bool HasReachableExit(ControlFlowNode node) => isSwitch
? loopContext.GetBreakTargets(node).Any()
: context.ControlFlowGraph.HasReachableExit(node);
/// <summary>
/// Returns the children in a loop dominator tree, with an optional exit point
/// Avoids returning continue statements when analysing switches (because increment blocks can be dominated)
/// </summary>
IEnumerable<ControlFlowNode> DominatorTreeChildren(ControlFlowNode n, ControlFlowNode exitPoint) =>
n.DominatorTreeChildren.Where(c => c != exitPoint && (!isSwitch || !loopContext.MatchContinue(c)));
/// <summary> /// <summary>
/// Pick exit point by picking any node that has no reachable exits. /// Pick exit point by picking any node that has no reachable exits.
/// ///
@ -397,9 +421,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// <remarks>This method must not write to the Visited flags on the CFG.</remarks> /// <remarks>This method must not write to the Visited flags on the CFG.</remarks>
void PickExitPoint(ControlFlowNode node, ref ControlFlowNode exitPoint, ref int exitPointILOffset) void PickExitPoint(ControlFlowNode node, ref ControlFlowNode exitPoint, ref int exitPointILOffset)
{ {
if (isSwitch && loopContext.MatchContinue(node))
return;
Block block = (Block)node.UserData; Block block = (Block)node.UserData;
if (block.ILRange.Start > exitPointILOffset if (block.ILRange.Start > exitPointILOffset
&& !context.ControlFlowGraph.HasReachableExit(node) && !HasReachableExit(node)
&& ((Block)node.UserData).Parent == currentBlockContainer) && ((Block)node.UserData).Parent == currentBlockContainer)
{ {
// HasReachableExit(node) == false // HasReachableExit(node) == false
@ -431,7 +458,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// or that leave the block Container. /// or that leave the block Container.
/// </summary> /// </summary>
/// <param name="loopHead">Entry point of the loop.</param> /// <param name="loopHead">Entry point of the loop.</param>
/// <param name="treatBackEdgesAsExits">Whether to treat loop back edges as exit points.</param> /// <param name="isSwitch">Whether to ignore branches that map to C# 'continue' statements.</param>
/// <param name="exitNodeArity">out: The number of different CFG nodes. /// <param name="exitNodeArity">out: The number of different CFG nodes.
/// Possible values: /// Possible values:
/// 0 = no CFG nodes used as exit nodes (although edges leaving the block container might still be exits); /// 0 = no CFG nodes used as exit nodes (although edges leaving the block container might still be exits);
@ -439,7 +466,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// 2 = more than one CFG node (not dominated by loopHead) was used as an exit node. /// 2 = more than one CFG node (not dominated by loopHead) was used as an exit node.
/// </param> /// </param>
/// <returns></returns> /// <returns></returns>
ControlFlowNode[] PrepareReverseCFG(ControlFlowNode loopHead, bool treatBackEdgesAsExits, out int exitNodeArity) ControlFlowNode[] PrepareReverseCFG(ControlFlowNode loopHead, out int exitNodeArity)
{ {
ControlFlowNode[] cfg = context.ControlFlowGraph.cfg; ControlFlowNode[] cfg = context.ControlFlowGraph.cfg;
ControlFlowNode[] rev = new ControlFlowNode[cfg.Length + 1]; ControlFlowNode[] rev = new ControlFlowNode[cfg.Length + 1];
@ -451,11 +478,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
ControlFlowNode exitNode = new ControlFlowNode { UserIndex = -1 }; ControlFlowNode exitNode = new ControlFlowNode { UserIndex = -1 };
rev[cfg.Length] = exitNode; rev[cfg.Length] = exitNode;
for (int i = 0; i < cfg.Length; i++) { for (int i = 0; i < cfg.Length; i++) {
if (!loopHead.Dominates(cfg[i])) if (!loopHead.Dominates(cfg[i]) || isSwitch && cfg[i] != loopHead && loopContext.MatchContinue(cfg[i]))
continue; continue;
// Add reverse edges for all edges in cfg // Add reverse edges for all edges in cfg
foreach (var succ in cfg[i].Successors) { foreach (var succ in cfg[i].Successors) {
if (loopHead.Dominates(succ) && (!treatBackEdgesAsExits || loopHead != succ)) { // edges to outer loops still count as exits (labelled continue not implemented)
if (isSwitch && loopContext.MatchContinue(succ, 1))
continue;
if (loopHead.Dominates(succ)) {
rev[succ.UserIndex].AddEdgeTo(rev[i]); rev[succ.UserIndex].AddEdgeTo(rev[i]);
} else { } else {
if (nodeTreatedAsExitNode == null) if (nodeTreatedAsExitNode == null)
@ -658,11 +690,14 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
Debug.Assert(h.UserData == block); Debug.Assert(h.UserData == block);
Debug.Assert(!TreeTraversal.PreOrder(h, n => n.DominatorTreeChildren).Any(n => n.Visited)); Debug.Assert(!TreeTraversal.PreOrder(h, n => n.DominatorTreeChildren).Any(n => n.Visited));
isSwitch = true;
loopContext = new SwitchDetection.LoopContext(context.ControlFlowGraph, h);
var nodesInSwitch = new List<ControlFlowNode>(); var nodesInSwitch = new List<ControlFlowNode>();
nodesInSwitch.Add(h); nodesInSwitch.Add(h);
h.Visited = true; h.Visited = true;
ExtendLoop(h, nodesInSwitch, out var exitPoint, isSwitch: true); ExtendLoop(h, nodesInSwitch, out var exitPoint);
if (exitPoint != null && exitPoint.Predecessors.Count == 1 && !context.ControlFlowGraph.HasReachableExit(exitPoint)) { if (exitPoint != null && h.Dominates(exitPoint) && exitPoint.Predecessors.Count == 1 && !HasReachableExit(exitPoint)) {
// If the exit point is reachable from just one single "break;", // If the exit point is reachable from just one single "break;",
// it's better to move the code into the switch. // it's better to move the code into the switch.
// (unlike loops which should not be nested unless necessary, // (unlike loops which should not be nested unless necessary,
@ -705,6 +740,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
branch.ReplaceWith(new Leave(switchContainer) { ILRange = branch.ILRange }); branch.ReplaceWith(new Leave(switchContainer) { ILRange = branch.ILRange });
} }
} }
isSwitch = false;
} }
} }
} }

34
ICSharpCode.Decompiler/IL/ControlFlow/SwitchAnalysis.cs

@ -3,6 +3,7 @@ using System.Diagnostics;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util; using ICSharpCode.Decompiler.Util;
using System; using System;
using System.Linq;
namespace ICSharpCode.Decompiler.IL.ControlFlow namespace ICSharpCode.Decompiler.IL.ControlFlow
{ {
@ -38,7 +39,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
public bool ContainsILSwitch { get; private set; } public bool ContainsILSwitch { get; private set; }
/// <summary> /// <summary>
/// Gets the sections that were detected by the previoous AnalyzeBlock() call. /// Gets the sections that were detected by the previous AnalyzeBlock() call.
/// </summary> /// </summary>
public readonly List<KeyValuePair<LongSet, ILInstruction>> Sections = new List<KeyValuePair<LongSet, ILInstruction>>(); public readonly List<KeyValuePair<LongSet, ILInstruction>> Sections = new List<KeyValuePair<LongSet, ILInstruction>>();
@ -58,8 +59,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// Blocks that can be deleted if the tail of the initial block is replaced with a switch instruction. /// Blocks that can be deleted if the tail of the initial block is replaced with a switch instruction.
/// </summary> /// </summary>
public readonly List<Block> InnerBlocks = new List<Block>(); public readonly List<Block> InnerBlocks = new List<Block>();
Block rootBlock; public Block RootBlock { get; private set; }
/// <summary> /// <summary>
/// Analyze the last two statements in the block and see if they can be turned into a /// Analyze the last two statements in the block and see if they can be turned into a
@ -69,7 +70,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
public bool AnalyzeBlock(Block block) public bool AnalyzeBlock(Block block)
{ {
switchVar = null; switchVar = null;
rootBlock = block; RootBlock = block;
targetBlockToSectionIndex.Clear(); targetBlockToSectionIndex.Clear();
targetContainerToSectionIndex.Clear(); targetContainerToSectionIndex.Clear();
Sections.Clear(); Sections.Clear();
@ -99,12 +100,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return false; return false;
} }
if (tailOnly) { if (tailOnly) {
Debug.Assert(block == rootBlock); Debug.Assert(block == RootBlock);
} else { } else {
Debug.Assert(switchVar != null); // switchVar should always be determined by the top-level call Debug.Assert(switchVar != null); // switchVar should always be determined by the top-level call
if (block.IncomingEdgeCount != 1 || block == rootBlock) if (block.IncomingEdgeCount != 1 || block == RootBlock)
return false; // for now, let's only consider if-structures that form a tree return false; // for now, let's only consider if-structures that form a tree
if (block.Parent != rootBlock.Parent) if (block.Parent != RootBlock.Parent)
return false; // all blocks should belong to the same container return false; // all blocks should belong to the same container
} }
LongSet trueValues; LongSet trueValues;
@ -230,15 +231,30 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return inst.MatchLdLoc(out switchVar); return inst.MatchLdLoc(out switchVar);
} }
bool MatchSwitchVar(ILInstruction inst, out long sub)
{
if (inst is BinaryNumericInstruction bn
&& bn.Operator == BinaryNumericOperator.Sub
&& !bn.CheckForOverflow && !bn.IsLifted
&& bn.Right.MatchLdcI(out sub))
{
return MatchSwitchVar(bn.Left);
}
sub = 0;
return MatchSwitchVar(inst);
}
/// <summary> /// <summary>
/// Analyzes the boolean condition, returning the set of values of the interesting /// Analyzes the boolean condition, returning the set of values of the interesting
/// variable for which the condition evaluates to true. /// variable for which the condition evaluates to true.
/// </summary> /// </summary>
private bool AnalyzeCondition(ILInstruction condition, out LongSet trueValues) private bool AnalyzeCondition(ILInstruction condition, out LongSet trueValues)
{ {
if (condition is Comp comp && MatchSwitchVar(comp.Left) && comp.Right.MatchLdcI(out long val)) { if (condition is Comp comp && MatchSwitchVar(comp.Left, out var sub) && comp.Right.MatchLdcI(out long val)) {
// if (comp(V OP val)) // if (comp((V - sub) OP val))
trueValues = MakeSetWhereComparisonIsTrue(comp.Kind, val, comp.Sign); trueValues = MakeSetWhereComparisonIsTrue(comp.Kind, val, comp.Sign);
trueValues = trueValues.AddOffset(sub);
return true; return true;
} else if (MatchSwitchVar(condition)) { } else if (MatchSwitchVar(condition)) {
// if (ldloc V) --> branch for all values except 0 // if (ldloc V) --> branch for all values except 0

356
ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs

@ -21,6 +21,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Diagnostics; using System.Diagnostics;
using ICSharpCode.Decompiler.FlowAnalysis;
using ICSharpCode.Decompiler.Util; using ICSharpCode.Decompiler.Util;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
@ -35,11 +36,106 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// </summary> /// </summary>
class SwitchDetection : IILTransform class SwitchDetection : IILTransform
{ {
SwitchAnalysis analysis = new SwitchAnalysis(); private readonly SwitchAnalysis analysis = new SwitchAnalysis();
private ILTransformContext context;
private BlockContainer currentContainer;
private ControlFlowGraph controlFlowGraph;
private LoopContext loopContext;
/// <summary>
/// When detecting a switch, it is important to distinguish Branch instructions which will
/// eventually decompile to continue; statements.
///
/// A LoopContext is constructed for a node and its dominator tree, as for a Branch to be a continue;
/// statement, it must be contained within the target-loop
///
/// This class also supplies the depth of the loop targetted by a continue; statement relative to the
/// context node, to avoid (or eventually support) labelled continues to outer loops
/// </summary>
public class LoopContext
{
private readonly IDictionary<ControlFlowNode, int> continueDepth = new Dictionary<ControlFlowNode, int>();
public LoopContext(ControlFlowGraph cfg, ControlFlowNode contextNode)
{
var loopHeads = new List<ControlFlowNode>();
void Analyze(ControlFlowNode n)
{
if (n.Visited)
return;
n.Visited = true;
if (n.Dominates(contextNode))
loopHeads.Add(n);
else
n.Successors.ForEach(Analyze);
}
contextNode.Successors.ForEach(Analyze);
ResetVisited(cfg.cfg);
int l = 1;
foreach (var loopHead in loopHeads.OrderBy(n => n.PostOrderNumber))
continueDepth[FindContinue(loopHead)] = l++;
}
private static ControlFlowNode FindContinue(ControlFlowNode loopHead)
{
// potential continue target
var pred = loopHead.Predecessors.OnlyOrDefault(p => p != loopHead && loopHead.Dominates(p));
if (pred == null)
return loopHead;
// match for loop increment block
if (pred.Successors.Count == 1) {
if (HighLevelLoopTransform.MatchIncrementBlock((Block)pred.UserData, out var target) &&target == loopHead.UserData)
return pred;
}
// match do-while condition
if (pred.Successors.Count <= 2) {
if (HighLevelLoopTransform.MatchDoWhileConditionBlock((Block)pred.UserData, out var t1, out var t2) &&
(t1 == loopHead.UserData || t2 == loopHead.UserData))
return pred;
}
return loopHead;
}
public bool MatchContinue(ControlFlowNode node) => MatchContinue(node, out var _);
public bool MatchContinue(ControlFlowNode node, int depth) =>
MatchContinue(node, out int _depth) && depth == _depth;
public bool MatchContinue(ControlFlowNode node, out int depth) => continueDepth.TryGetValue(node, out depth);
public int GetContinueDepth(ControlFlowNode node) => MatchContinue(node, out var depth) ? depth : 0;
/// <summary>
/// Lists all potential targets for break; statements from a domination tree,
/// assuming the domination tree must be exited via either break; or continue;
///
/// First list all nodes in the dominator tree (excluding continue nodes)
/// Then return the all successors not contained within said tree.
///
/// Note that node will be returned once for each outgoing edge.
/// Labelled continue statements (depth > 1) are counted as break targets
/// </summary>
internal IEnumerable<ControlFlowNode> GetBreakTargets(ControlFlowNode dominator) =>
TreeTraversal.PreOrder(dominator, n => n.DominatorTreeChildren.Where(c => !MatchContinue(c)))
.SelectMany(n => n.Successors)
.Where(n => !dominator.Dominates(n) && !MatchContinue(n, 1));
}
public void Run(ILFunction function, ILTransformContext context) public void Run(ILFunction function, ILTransformContext context)
{ {
this.context = context;
foreach (var container in function.Descendants.OfType<BlockContainer>()) { foreach (var container in function.Descendants.OfType<BlockContainer>()) {
currentContainer = container;
controlFlowGraph = null;
bool blockContainerNeedsCleanup = false; bool blockContainerNeedsCleanup = false;
foreach (var block in container.Blocks) { foreach (var block in container.Blocks) {
context.CancellationToken.ThrowIfCancellationRequested(); context.CancellationToken.ThrowIfCancellationRequested();
@ -47,7 +143,14 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
} }
if (blockContainerNeedsCleanup) { if (blockContainerNeedsCleanup) {
Debug.Assert(container.Blocks.All(b => b.Instructions.Count != 0 || b.IncomingEdgeCount == 0)); Debug.Assert(container.Blocks.All(b => b.Instructions.Count != 0 || b.IncomingEdgeCount == 0));
container.Blocks.RemoveAll(b => b.Instructions.Count == 0);
// if the original code has an unreachable switch-like condition
// eg. if (i >= 0) { ... } else if (i == 2) { unreachable }
// then the 'i == 2' block head gets consumed and the unreachable code needs deleting
if (context.Settings.RemoveDeadCode)
container.SortBlocks(deleteUnreachableBlocks: true);
else
container.Blocks.RemoveAll(b => b.Instructions.Count == 0);
} }
} }
} }
@ -56,7 +159,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
{ {
bool analysisSuccess = analysis.AnalyzeBlock(block); bool analysisSuccess = analysis.AnalyzeBlock(block);
KeyValuePair<LongSet, ILInstruction> defaultSection; KeyValuePair<LongSet, ILInstruction> defaultSection;
if (analysisSuccess && UseCSharpSwitch(analysis, out defaultSection)) { if (analysisSuccess && UseCSharpSwitch(out defaultSection)) {
// complex multi-block switch that can be combined into a single SwitchInstruction // complex multi-block switch that can be combined into a single SwitchInstruction
ILInstruction switchValue = new LdLoc(analysis.SwitchVariable); ILInstruction switchValue = new LdLoc(analysis.SwitchVariable);
if (switchValue.ResultType == StackType.Unknown) { if (switchValue.ResultType == StackType.Unknown) {
@ -85,6 +188,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
Debug.Assert(innerBlock != ((BlockContainer)block.Parent).EntryPoint); Debug.Assert(innerBlock != ((BlockContainer)block.Parent).EntryPoint);
innerBlock.Instructions.Clear(); innerBlock.Instructions.Clear();
} }
controlFlowGraph = null; // control flow graph is no-longer valid
blockContainerNeedsCleanup = true; blockContainerNeedsCleanup = true;
SortSwitchSections(sw); SortSwitchSections(sw);
} else { } else {
@ -151,15 +256,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
} }
} }
const ulong MaxValuesPerSection = 50; const ulong MaxValuesPerSection = 100;
/// <summary> /// <summary>
/// Tests whether we should prefer a switch statement over an if statement. /// Tests whether we should prefer a switch statement over an if statement.
/// </summary> /// </summary>
static bool UseCSharpSwitch(SwitchAnalysis analysis, out KeyValuePair<LongSet, ILInstruction> defaultSection) private bool UseCSharpSwitch(out KeyValuePair<LongSet, ILInstruction> defaultSection)
{ {
if (!analysis.InnerBlocks.Any()) { if (!analysis.InnerBlocks.Any()) {
defaultSection = default(KeyValuePair<LongSet, ILInstruction>); defaultSection = default;
return false; return false;
} }
defaultSection = analysis.Sections.FirstOrDefault(s => s.Key.Count() > MaxValuesPerSection); defaultSection = analysis.Sections.FirstOrDefault(s => s.Key.Count() > MaxValuesPerSection);
@ -168,24 +273,239 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// This should never happen, as we'd need 2^64/MaxValuesPerSection sections to hit this case... // This should never happen, as we'd need 2^64/MaxValuesPerSection sections to hit this case...
return false; return false;
} }
ulong valuePerSectionLimit = MaxValuesPerSection;
if (!analysis.ContainsILSwitch) {
// If there's no IL switch involved, limit the number of keys per section
// much more drastically to avoid generating switches where an if condition
// would be shorter.
valuePerSectionLimit = Math.Min(
valuePerSectionLimit,
(ulong)analysis.InnerBlocks.Count);
}
var defaultSectionKey = defaultSection.Key; var defaultSectionKey = defaultSection.Key;
if (analysis.Sections.Any(s => !s.Key.SetEquals(defaultSectionKey) if (analysis.Sections.Any(s => !s.Key.SetEquals(defaultSectionKey) && s.Key.Count() > MaxValuesPerSection)) {
&& s.Key.Count() > valuePerSectionLimit)) {
// Only the default section is allowed to have tons of keys. // Only the default section is allowed to have tons of keys.
// C# doesn't support "case 1 to 100000000", and we don't want to generate // C# doesn't support "case 1 to 100000000", and we don't want to generate
// gigabytes of case labels. // gigabytes of case labels.
return false; return false;
} }
return true;
// good enough indicator that the surrounding code also forms a switch statement
if (analysis.ContainsILSwitch || MatchRoslynSwitchOnString())
return true;
// heuristic to determine if a block would be better represented as an if statement rather than switch
int ifCount = analysis.InnerBlocks.Count + 1;
int intervalCount = analysis.Sections.Where(s => !s.Key.SetEquals(defaultSectionKey)).Sum(s => s.Key.Intervals.Length);
if (ifCount < intervalCount)
return false;
(var flowNodes, var caseNodes) = AnalyzeControlFlow();
// don't create switch statements with only one non-default label when the corresponding condition tree is flat
// it may be important that the switch-like conditions be inlined
// for example, a loop condition: while (c == '\n' || c == '\r')
if (analysis.Sections.Count == 2 && IsSingleCondition(flowNodes, caseNodes))
return false;
// if there is no ILSwitch, there's still many control flow patterns that
// match a switch statement but were originally just regular if statements,
// and converting them to switches results in poor quality code with goto statements
//
// If a single break target cannot be identified, then the equivalent switch statement would require goto statements.
// These goto statements may be "goto case x" or "goto default", but these are a hint that the original code was not a switch,
// and that the switch statement may be very poor quality.
// Thus the rule of thumb is no goto statements if the original code didn't include them
if (SwitchUsesGoto(flowNodes, caseNodes, out var breakBlock))
return false;
// valid switch construction, all code can be inlined
if (breakBlock == null)
return true;
// The switch has a single break target and there is one more hint
// The break target cannot be inlined, and should have the highest IL offset of everything targetted by the switch
return breakBlock.ILRange.Start >= analysis.Sections.Select(s => s.Value.MatchBranch(out var b) ? b.ILRange.Start : -1).Max();
}
/// <summary>
/// stloc switchValueVar(call ComputeStringHash(switchValue))
/// </summary>
private bool MatchRoslynSwitchOnString()
{
var insns = analysis.RootBlock.Instructions;
return insns.Count >= 3 && SwitchOnStringTransform.MatchComputeStringHashCall(insns[insns.Count - 3], analysis.SwitchVariable, out var switchLdLoc);
}
/// <summary>
/// Builds the control flow graph for the current container (if necessary), establishes loopContext
/// and returns the ControlFlowNodes corresponding to the inner flow and case blocks of the potential switch
/// </summary>
private (List<ControlFlowNode> flowNodes, List<ControlFlowNode> caseNodes) AnalyzeControlFlow()
{
if (controlFlowGraph == null)
controlFlowGraph = new ControlFlowGraph(currentContainer, context.CancellationToken);
var switchHead = controlFlowGraph.GetNode(analysis.RootBlock);
loopContext = new LoopContext(controlFlowGraph, switchHead);
var flowNodes = new List<ControlFlowNode> { switchHead };
flowNodes.AddRange(analysis.InnerBlocks.Select(controlFlowGraph.GetNode));
// grab the control flow nodes for blocks targetted by each section
var caseNodes = new List<ControlFlowNode>();
foreach (var s in analysis.Sections) {
if (!s.Value.MatchBranch(out var block))
continue;
var node = controlFlowGraph.GetNode(block);
if (!loopContext.MatchContinue(node))
caseNodes.Add(node);
}
AddNullCase(flowNodes, caseNodes);
Debug.Assert(flowNodes.SelectMany(n => n.Successors)
.All(n => flowNodes.Contains(n) || caseNodes.Contains(n) || loopContext.MatchContinue(n)));
return (flowNodes, caseNodes);
}
/// <summary>
/// Determines if the analysed switch can be constructed without any gotos
/// </summary>
private bool SwitchUsesGoto(List<ControlFlowNode> flowNodes, List<ControlFlowNode> caseNodes, out Block breakBlock)
{
// cases with predecessors that aren't part of the switch logic
// must either require "goto case" statements, or consist of a single "break;"
var externalCases = caseNodes.Where(c => c.Predecessors.Any(n => !flowNodes.Contains(n))).ToList();
breakBlock = null;
if (externalCases.Count > 1)
return true; // cannot have more than one break case without gotos
// check that case nodes flow through a single point
var breakTargets = caseNodes.Except(externalCases).SelectMany(n => loopContext.GetBreakTargets(n)).ToHashSet();
// if there are multiple break targets, then gotos are required
// if there are none, then the external case (if any) can be the break target
if (breakTargets.Count != 1)
return breakTargets.Count > 1;
breakBlock = (Block) breakTargets.Single().UserData;
// external case must consist of a single "break;"
return externalCases.Count == 1 && breakBlock != externalCases.Single().UserData;
}
/// <summary>
/// Does some of the analysis of SwitchOnNullableTransform to add the null case control flow
/// to the results of SwitchAnaylsis
/// </summary>
private void AddNullCase(List<ControlFlowNode> flowNodes, List<ControlFlowNode> caseNodes)
{
if (analysis.RootBlock.IncomingEdgeCount != 1)
return;
// if (comp(logic.not(call get_HasValue(ldloca nullableVar))) br NullCase
// br RootBlock
var nullableBlock = (Block)controlFlowGraph.GetNode(analysis.RootBlock).Predecessors.SingleOrDefault()?.UserData;
if (nullableBlock == null ||
nullableBlock.Instructions.Count < 2 ||
!nullableBlock.Instructions.Last().MatchBranch(analysis.RootBlock) ||
!nullableBlock.Instructions.SecondToLastOrDefault().MatchIfInstruction(out var cond, out var trueInst) ||
!cond.MatchLogicNot(out var getHasValue) ||
!NullableLiftingTransform.MatchHasValueCall(getHasValue, out ILInstruction nullableInst))
return;
// could check that nullableInst is ldloc or ldloca and that the switch variable matches a GetValueOrDefault
// but the effect of adding an incorrect block to the flowBlock list would only be disasterous if it branched directly
// to a candidate case block
// must branch to a case label, otherwise we can proceed fine and let SwitchOnNullableTransform do all the work
if (!trueInst.MatchBranch(out var nullBlock) || !caseNodes.Exists(n => n.UserData == nullBlock))
return;
//add the null case logic to the incoming flow blocks
flowNodes.Add(controlFlowGraph.GetNode(nullableBlock));
}
/// <summary>
/// Pattern matching for short circuit expressions
/// p
/// |\
/// | n
/// |/ \
/// s c
///
/// where
/// p: if (a) goto n; goto s;
/// n: if (b) goto c; goto s;
///
/// Can simplify to
/// p|n
/// / \
/// s c
///
/// where:
/// p|n: if (a && b) goto c; goto s;
///
/// Note that if n has only 1 successor, but is still a flow node, then a short circuit expression
/// has a target (c) with no corresponding block (leave)
/// </summary>
/// <param name="parent">A node with 2 successors</param>
/// <param name="side">The successor index to consider n (the other successor will be the common sibling)</param>
private static bool IsShortCircuit(ControlFlowNode parent, int side)
{
var node = parent.Successors[side];
var sibling = parent.Successors[side ^ 1];
if (!IsFlowNode(node) || node.Successors.Count > 2 || node.Predecessors.Count != 1)
return false;
return node.Successors.Contains(sibling);
}
/// <summary>
/// A flow node contains only two instructions, the first of which is an IfInstruction
/// A short circuit expression is comprised of a root block ending in an IfInstruction and one or more flow nodes
/// </summary>
static bool IsFlowNode(ControlFlowNode n) => ((Block)n.UserData).Instructions.FirstOrDefault() is IfInstruction;
/// <summary>
/// Determines whether the flowNodes are can be reduced to a single condition via short circuit operators
/// </summary>
private bool IsSingleCondition(List<ControlFlowNode> flowNodes, List<ControlFlowNode> caseNodes)
{
if (flowNodes.Count == 1)
return true;
var rootNode = controlFlowGraph.GetNode(analysis.RootBlock);
rootNode.Visited = true;
// search down the tree, marking nodes as visited while they continue the current condition
var n = rootNode;
while (n.Successors.Count > 0 && (n == rootNode || IsFlowNode(n))) {
if (n.Successors.Count == 1) {
// if there is more than one case node, then a flow node with only one successor is not part of the initial condition
if (caseNodes.Count > 1)
break;
n = n.Successors[0];
}
else { // 2 successors
if (IsShortCircuit(n, 0))
n = n.Successors[0];
else if (IsShortCircuit(n, 1))
n = n.Successors[1];
else
break;
}
n.Visited = true;
if (loopContext.MatchContinue(n))
break;
}
var ret = flowNodes.All(f => f.Visited);
ResetVisited(controlFlowGraph.cfg);
return ret;
}
private static void ResetVisited(IEnumerable<ControlFlowNode> nodes)
{
foreach (var n in nodes)
n.Visited = false;
} }
} }
} }

61
ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs

@ -57,17 +57,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// if (!loop-condition) leave loop-container // if (!loop-condition) leave loop-container
// ... // ...
condition = null; condition = null;
loopBody = null; loopBody = loop.EntryPoint;
if (!(loop.EntryPoint.Instructions[0] is IfInstruction ifInstruction)) if (!(loopBody.Instructions[0] is IfInstruction ifInstruction))
return false; return false;
if (!ifInstruction.FalseInst.MatchNop()) if (!ifInstruction.FalseInst.MatchNop())
return false; return false;
if (UsesVariableCapturedInLoop(loop, ifInstruction.Condition)) if (UsesVariableCapturedInLoop(loop, ifInstruction.Condition))
return false; return false;
condition = ifInstruction; condition = ifInstruction;
if (!ifInstruction.TrueInst.MatchLeave(loop)) if (!ifInstruction.TrueInst.MatchLeave(loop)) {
return false; // sometimes the loop-body is nested within the if
// if (loop-condition) { loop-body }
// leave loop-container
if (loopBody.Instructions.Count != 2 || !loop.EntryPoint.Instructions.Last().MatchLeave(loop))
return false;
if (!ifInstruction.TrueInst.HasFlag(InstructionFlags.EndPointUnreachable))
((Block)ifInstruction.TrueInst).Instructions.Add(new Leave(loop));
ConditionDetection.InvertIf(loopBody, ifInstruction, context);
}
context.Step("Transform to while (condition) loop", loop); context.Step("Transform to while (condition) loop", loop);
loop.Kind = ContainerKind.While; loop.Kind = ContainerKind.While;
@ -95,26 +108,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
ExpressionTransforms.RunOnSingleStatment(inst, context); ExpressionTransforms.RunOnSingleStatment(inst, context);
} }
}*/ }*/
// Invert condition and unwrap nested block, if loop ends in a break or return statement preceeded by an IfInstruction.
/*while (loopBody.Instructions.Last() is Leave leave && loopBody.Instructions.SecondToLastOrDefault() is IfInstruction nestedIf && nestedIf.FalseInst.MatchNop()) {
switch (nestedIf.TrueInst) {
case Block nestedBlock:
loopBody.Instructions.RemoveAt(leave.ChildIndex);
loopBody.Instructions.AddRange(nestedBlock.Instructions);
break;
case Branch br:
leave.ReplaceWith(nestedIf.TrueInst);
break;
default:
return true;
}
nestedIf.Condition = Comp.LogicNot(nestedIf.Condition);
nestedIf.TrueInst = leave;
ExpressionTransforms.RunOnSingleStatment(nestedIf, context);
if (!loopBody.HasFlag(InstructionFlags.EndPointUnreachable))
loopBody.Instructions.Add(new Leave(loop));
}*/
return true; return true;
} }
@ -325,11 +319,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; return true;
} }
// early match before block containers have been constructed
internal static bool MatchDoWhileConditionBlock(Block block, out Block target1, out Block target2)
{
target1 = target2 = null;
if (block.Instructions.Count < 2)
return false;
var last = block.Instructions.Last();
if (!(block.Instructions.SecondToLastOrDefault() is IfInstruction ifInstruction) || !ifInstruction.FalseInst.MatchNop())
return false;
return (ifInstruction.TrueInst.MatchBranch(out target1) || ifInstruction.TrueInst.MatchReturn(out var _)) &&
(last.MatchBranch(out target2) || last.MatchReturn(out var _));
}
internal static Block GetIncrementBlock(BlockContainer loop, Block whileLoopBody) => internal static Block GetIncrementBlock(BlockContainer loop, Block whileLoopBody) =>
loop.Blocks.SingleOrDefault(b => b != whileLoopBody loop.Blocks.SingleOrDefault(b => b != whileLoopBody
&& b.Instructions.Last().MatchBranch(loop.EntryPoint) && b.Instructions.Last().MatchBranch(loop.EntryPoint)
&& b.Instructions.SkipLast(1).All(IsSimpleStatement)); && b.Instructions.SkipLast(1).All(IsSimpleStatement));
internal static bool MatchIncrementBlock(Block block, out Block loopHead) =>
block.Instructions.Last().MatchBranch(out loopHead)
&& block.Instructions.SkipLast(1).All(IsSimpleStatement);
bool MatchForLoop(BlockContainer loop, IfInstruction whileCondition, Block whileLoopBody) bool MatchForLoop(BlockContainer loop, IfInstruction whileCondition, Block whileLoopBody)
{ {
// for loops have exactly two incoming edges at the entry point. // for loops have exactly two incoming edges at the entry point.

2
ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs

@ -847,7 +847,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// <summary> /// <summary>
/// Matches 'stloc(targetVar, call ComputeStringHash(ldloc switchValue))' /// Matches 'stloc(targetVar, call ComputeStringHash(ldloc switchValue))'
/// </summary> /// </summary>
bool MatchComputeStringHashCall(ILInstruction inst, ILVariable targetVar, out LdLoc switchValue) internal static bool MatchComputeStringHashCall(ILInstruction inst, ILVariable targetVar, out LdLoc switchValue)
{ {
switchValue = null; switchValue = null;
if (!inst.MatchStLoc(targetVar, out var value)) if (!inst.MatchStLoc(targetVar, out var value))

47
ICSharpCode.Decompiler/Util/CollectionExtensions.cs

@ -200,27 +200,28 @@ namespace ICSharpCode.Decompiler.Util
/// </summary> /// </summary>
public static IEnumerable<T> Merge<T>(this IEnumerable<T> input1, IEnumerable<T> input2, Comparison<T> comparison) public static IEnumerable<T> Merge<T>(this IEnumerable<T> input1, IEnumerable<T> input2, Comparison<T> comparison)
{ {
var enumA = input1.GetEnumerator(); using (var enumA = input1.GetEnumerator())
var enumB = input2.GetEnumerator(); using (var enumB = input2.GetEnumerator()) {
bool moreA = enumA.MoveNext(); bool moreA = enumA.MoveNext();
bool moreB = enumB.MoveNext(); bool moreB = enumB.MoveNext();
while (moreA && moreB) { while (moreA && moreB) {
if (comparison(enumA.Current, enumB.Current) <= 0) { if (comparison(enumA.Current, enumB.Current) <= 0) {
yield return enumA.Current;
moreA = enumA.MoveNext();
} else {
yield return enumB.Current;
moreB = enumB.MoveNext();
}
}
while (moreA) {
yield return enumA.Current; yield return enumA.Current;
moreA = enumA.MoveNext(); moreA = enumA.MoveNext();
} else { }
while (moreB) {
yield return enumB.Current; yield return enumB.Current;
moreB = enumB.MoveNext(); moreB = enumB.MoveNext();
} }
} }
while (moreA) {
yield return enumA.Current;
moreA = enumA.MoveNext();
}
while (moreB) {
yield return enumB.Current;
moreB = enumB.MoveNext();
}
} }
/// <summary> /// <summary>
@ -306,6 +307,22 @@ namespace ICSharpCode.Decompiler.Util
list.RemoveAt(list.Count - 1); list.RemoveAt(list.Count - 1);
} }
public static T OnlyOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate) => OnlyOrDefault(source.Where(predicate));
public static T OnlyOrDefault<T>(this IEnumerable<T> source)
{
bool any = false;
T first = default;
foreach (var t in source) {
if (any)
return default(T);
first = t;
any = true;
}
return first;
}
#region Aliases/shortcuts for Enumerable extension methods #region Aliases/shortcuts for Enumerable extension methods
public static bool Any<T>(this ICollection<T> list) => list.Count > 0; public static bool Any<T>(this ICollection<T> list) => list.Count > 0;
public static bool Any<T>(this T[] array, Predicate<T> match) => Array.Exists(array, match); public static bool Any<T>(this T[] array, Predicate<T> match) => Array.Exists(array, match);

3
ICSharpCode.Decompiler/Util/LongSet.cs

@ -203,6 +203,9 @@ namespace ICSharpCode.Decompiler.Util
/// </summary> /// </summary>
public LongSet AddOffset(long val) public LongSet AddOffset(long val)
{ {
if (val == 0) {
return this;
}
var newIntervals = new List<LongInterval>(Intervals.Length + 1); var newIntervals = new List<LongInterval>(Intervals.Length + 1);
foreach (var element in Intervals) { foreach (var element in Intervals) {
long newStart = unchecked(element.Start + val); long newStart = unchecked(element.Start + val);

Loading…
Cancel
Save