Browse Source

Fix #2379: Keep `return` statements around in original form for ConditionDetection, only transform to fall-through block-exit at the end of the transform pipeline.

This fixes an issue where `return` statements within try-blocks could turn into `goto` statements.
pull/2425/head
Daniel Grunwald 4 years ago
parent
commit
6757295b3b
  1. 24
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs
  2. 27
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs
  3. 3
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  4. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  5. 2
      ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs
  6. 5
      ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs
  7. 185
      ICSharpCode.Decompiler/IL/ControlFlow/RemoveRedundantReturn.cs
  8. 16
      ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs
  9. 6
      ICSharpCode.Decompiler/IL/Instructions/Leave.cs
  10. 26
      ICSharpCode.Decompiler/IL/Transforms/HighLevelLoopTransform.cs
  11. 10
      ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs
  12. 2
      ILSpy/LoadedAssembly.cs

24
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExceptionHandling.cs

@ -246,6 +246,28 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
} }
internal void EarlyReturnInTryBlock(bool a, bool b)
{
try
{
if (a)
{
Console.WriteLine("a");
}
else if (b)
{
// #2379: The only goto-free way of representing this code is to use a return statement
return;
}
Console.WriteLine("a || !b");
}
finally
{
Console.WriteLine("finally");
}
}
#if ROSLYN || !OPT #if ROSLYN || !OPT
// TODO Non-Roslyn compilers create a second while loop inside the try, by inverting the if // TODO Non-Roslyn compilers create a second while loop inside the try, by inverting the if
// This is fixed in the non-optimised version by the enabling the RemoveDeadCode flag // This is fixed in the non-optimised version by the enabling the RemoveDeadCode flag
@ -476,4 +498,4 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
#endif #endif
} }
} }

27
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs

@ -198,17 +198,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
case 1: case 1:
Console.WriteLine("case 1"); Console.WriteLine("case 1");
return; return;
default:
if (B(1))
{
Console.WriteLine(1);
}
return;
} }
if (B(1))
{
Console.WriteLine(1);
}
}
else
{
Console.WriteLine("else");
} }
Console.WriteLine("else");
} }
// nesting should not be reduced as maximum nesting level is 1 // nesting should not be reduced as maximum nesting level is 1
@ -348,12 +346,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine(); Console.WriteLine();
if (B(1)) if (!B(1))
{ {
for (int i = 0; i < 10; i++) return;
{ }
Console.WriteLine(i); for (int i = 0; i < 10; i++)
} {
Console.WriteLine(i);
} }
} }
catch catch

3
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -117,7 +117,7 @@ namespace ICSharpCode.Decompiler.CSharp
} }
}, },
// re-run DetectExitPoints after loop detection // re-run DetectExitPoints after loop detection
new DetectExitPoints(canIntroduceExitForReturn: true), new DetectExitPoints(canIntroduceExitForReturn: false),
new BlockILTransform { // per-block transforms new BlockILTransform { // per-block transforms
PostOrderTransforms = { PostOrderTransforms = {
new ConditionDetection(), new ConditionDetection(),
@ -159,6 +159,7 @@ namespace ICSharpCode.Decompiler.CSharp
new TransformDisplayClassUsage(), new TransformDisplayClassUsage(),
new HighLevelLoopTransform(), new HighLevelLoopTransform(),
new ReduceNestingTransform(), new ReduceNestingTransform(),
new RemoveRedundantReturn(),
new IntroduceDynamicTypeOnLocals(), new IntroduceDynamicTypeOnLocals(),
new IntroduceNativeIntTypeOnLocals(), new IntroduceNativeIntTypeOnLocals(),
new AssignVariableNames(), new AssignVariableNames(),

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -79,6 +79,7 @@
<Compile Include="CSharp\Syntax\VariableDesignation.cs" /> <Compile Include="CSharp\Syntax\VariableDesignation.cs" />
<Compile Include="Humanizer\Vocabularies.cs" /> <Compile Include="Humanizer\Vocabularies.cs" />
<Compile Include="Humanizer\Vocabulary.cs" /> <Compile Include="Humanizer\Vocabulary.cs" />
<Compile Include="IL\ControlFlow\RemoveRedundantReturn.cs" />
<Compile Include="IL\Transforms\DeconstructionTransform.cs" /> <Compile Include="IL\Transforms\DeconstructionTransform.cs" />
<Compile Include="IL\Transforms\FixLoneIsInst.cs" /> <Compile Include="IL\Transforms\FixLoneIsInst.cs" />
<Compile Include="IL\Instructions\DeconstructInstruction.cs" /> <Compile Include="IL\Instructions\DeconstructInstruction.cs" />

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

@ -16,7 +16,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -366,7 +365,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
Debug.Assert(ifInst.Parent == block); Debug.Assert(ifInst.Parent == block);
//assert then block terminates //assert then block terminates
var trueExitInst = GetExit(ifInst.TrueInst);
var exitInst = GetExit(block); var exitInst = GetExit(block);
context.Step($"InvertIf at IL_{ifInst.StartILOffset:x4}", ifInst); context.Step($"InvertIf at IL_{ifInst.StartILOffset:x4}", ifInst);

5
ICSharpCode.Decompiler/IL/ControlFlow/ExitPoints.cs

@ -16,7 +16,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
@ -76,7 +75,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
else if (slot == TryInstruction.TryBlockSlot else if (slot == TryInstruction.TryBlockSlot
|| slot == TryCatchHandler.BodySlot || slot == TryCatchHandler.BodySlot
|| slot == TryCatch.HandlerSlot || slot == TryCatch.HandlerSlot
|| slot == PinnedRegion.BodySlot) || slot == PinnedRegion.BodySlot
|| slot == UsingInstruction.BodySlot
|| slot == LockInstruction.BodySlot)
{ {
return GetExit(inst.Parent); return GetExit(inst.Parent);
} }

185
ICSharpCode.Decompiler/IL/ControlFlow/RemoveRedundantReturn.cs

@ -0,0 +1,185 @@
// Copyright (c) Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System.Diagnostics;
using System.Linq;
using ICSharpCode.Decompiler.IL.Transforms;
namespace ICSharpCode.Decompiler.IL.ControlFlow
{
/// <summary>
/// Similar to <see cref="DetectExitPoints"/>, but acts only on <c>leave</c> instructions
/// leaving the whole function (<c>return</c>/<c>yield break</c>) that can be made implicit
/// without using goto.
/// </summary>
class RemoveRedundantReturn : IILTransform
{
public void Run(ILFunction function, ILTransformContext context)
{
foreach (var lambda in function.Descendants.OfType<ILFunction>())
{
if (lambda.Body is BlockContainer c && ((lambda.AsyncReturnType ?? lambda.ReturnType).Kind == TypeSystem.TypeKind.Void || lambda.IsIterator))
{
Block lastBlock = c.Blocks.Last();
if (lastBlock.Instructions.Last() is Leave { IsLeavingFunction: true })
{
ConvertReturnToFallthrough(lastBlock.Instructions.SecondToLastOrDefault());
}
else
{
if (ConvertReturnToFallthrough(lastBlock.Instructions.Last()))
{
lastBlock.Instructions.Add(new Leave(c));
}
}
}
}
}
private static bool ConvertReturnToFallthrough(ILInstruction? inst)
{
bool result = false;
switch (inst)
{
case BlockContainer c:
if (c.Kind != ContainerKind.Normal)
{
// loop or switch: turn all "return" into "break"
result |= ReturnToLeaveInContainer(c);
}
else
{
// body of try block, or similar: recurse into last instruction in container
Block lastBlock = c.Blocks.Last();
if (lastBlock.Instructions.Last() is Leave { IsLeavingFunction: true, Value: Nop } leave)
{
leave.TargetContainer = c;
result = true;
}
else if (ConvertReturnToFallthrough(lastBlock.Instructions.Last()))
{
lastBlock.Instructions.Add(new Leave(c));
result = true;
}
}
break;
case TryCatch tryCatch:
result |= ConvertReturnToFallthrough(tryCatch.TryBlock);
foreach (var h in tryCatch.Handlers)
{
result |= ConvertReturnToFallthrough(h.Body);
}
break;
case TryFinally tryFinally:
result |= ConvertReturnToFallthrough(tryFinally.TryBlock);
break;
case LockInstruction lockInst:
result |= ConvertReturnToFallthrough(lockInst.Body);
break;
case UsingInstruction usingInst:
result |= ConvertReturnToFallthrough(usingInst.Body);
break;
case PinnedRegion pinnedRegion:
result |= ConvertReturnToFallthrough(pinnedRegion.Body);
break;
case IfInstruction ifInstruction:
result |= ConvertReturnToFallthrough(ifInstruction.TrueInst);
result |= ConvertReturnToFallthrough(ifInstruction.FalseInst);
break;
case Block block when block.Kind == BlockKind.ControlFlow:
{
var lastInst = block.Instructions.LastOrDefault();
if (lastInst is Leave { IsLeavingFunction: true, Value: Nop })
{
block.Instructions.RemoveAt(block.Instructions.Count - 1);
result = true;
lastInst = block.Instructions.LastOrDefault();
}
result |= ConvertReturnToFallthrough(lastInst);
break;
}
}
return result;
}
/// <summary>
/// Transforms
/// loop { ... if (x) return; .. }
/// to
/// loop { ... if (x) break; .. } return;
/// </summary>
internal static void ReturnToBreak(Block parentBlock, BlockContainer loopOrSwitch, ILTransformContext context)
{
Debug.Assert(loopOrSwitch.Parent == parentBlock);
// This transform is only possible when the loop/switch doesn't already use "break;"
if (loopOrSwitch.LeaveCount != 0)
return;
// loopOrSwitch with LeaveCount==0 has unreachable exit point and thus must be last in block.
Debug.Assert(parentBlock.Instructions.Last() == loopOrSwitch);
var nearestFunction = parentBlock.Ancestors.OfType<ILFunction>().First();
if (nearestFunction.Body is BlockContainer functionContainer)
{
context.Step("pull return out of loop/switch: " + loopOrSwitch.EntryPoint.Label, loopOrSwitch);
if (ReturnToLeaveInContainer(loopOrSwitch))
{
// insert a return after the loop
parentBlock.Instructions.Add(new Leave(functionContainer));
}
}
}
private static bool ReturnToLeaveInContainer(BlockContainer c)
{
bool result = false;
foreach (var block in c.Blocks)
{
result |= ReturnToLeave(block, c);
}
return result;
}
private static bool ReturnToLeave(ILInstruction inst, BlockContainer c)
{
if (inst is Leave { IsLeavingFunction: true, Value: Nop } leave)
{
leave.TargetContainer = c;
return true;
}
else if (inst is BlockContainer nested && nested.Kind != ContainerKind.Normal)
{
return false;
}
else if (inst is ILFunction)
{
return false;
}
else
{
bool b = false;
foreach (var child in inst.Children)
{
b |= ReturnToLeave(child, c);
}
return b;
}
}
}
}

16
ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs

@ -16,6 +16,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -73,7 +75,7 @@ namespace ICSharpCode.Decompiler.IL
public struct Enumerator : IEnumerator<T> public struct Enumerator : IEnumerator<T>
{ {
#if DEBUG #if DEBUG
ILInstruction parentInstruction; ILInstruction? parentInstruction;
#endif #endif
readonly List<T> list; readonly List<T> list;
int pos; int pos;
@ -140,7 +142,7 @@ namespace ICSharpCode.Decompiler.IL
/// Runs in O(1) if the item can be found using the Parent/ChildIndex properties. /// Runs in O(1) if the item can be found using the Parent/ChildIndex properties.
/// Otherwise, runs in O(N). /// Otherwise, runs in O(N).
/// </remarks> /// </remarks>
public int IndexOf(T item) public int IndexOf(T? item)
{ {
if (item == null) if (item == null)
{ {
@ -163,7 +165,7 @@ namespace ICSharpCode.Decompiler.IL
/// This method searches the list. /// This method searches the list.
/// Usually it's more efficient to test item.Parent instead! /// Usually it's more efficient to test item.Parent instead!
/// </remarks> /// </remarks>
public bool Contains(T item) public bool Contains(T? item)
{ {
return IndexOf(item) >= 0; return IndexOf(item) >= 0;
} }
@ -395,7 +397,7 @@ namespace ICSharpCode.Decompiler.IL
return list[0]; return list[0];
} }
public T FirstOrDefault() public T? FirstOrDefault()
{ {
return list.Count > 0 ? list[0] : null; return list.Count > 0 ? list[0] : null;
} }
@ -405,17 +407,17 @@ namespace ICSharpCode.Decompiler.IL
return list[list.Count - 1]; return list[list.Count - 1];
} }
public T LastOrDefault() public T? LastOrDefault()
{ {
return list.Count > 0 ? list[list.Count - 1] : null; return list.Count > 0 ? list[list.Count - 1] : null;
} }
public T SecondToLastOrDefault() public T? SecondToLastOrDefault()
{ {
return list.Count > 1 ? list[list.Count - 2] : null; return list.Count > 1 ? list[list.Count - 2] : null;
} }
public T ElementAtOrDefault(int index) public T? ElementAtOrDefault(int index)
{ {
if (index >= 0 && index < list.Count) if (index >= 0 && index < list.Count)
return list[index]; return list[index];

6
ICSharpCode.Decompiler/IL/Instructions/Leave.cs

@ -83,11 +83,15 @@ namespace ICSharpCode.Decompiler.IL
} }
/// <summary> /// <summary>
/// Gets whether the leave instruction is leaving the whole ILFunction. /// Gets whether the leave instruction is directly leaving the whole ILFunction.
/// (TargetContainer == main container of the function). /// (TargetContainer == main container of the function).
/// ///
/// This is only valid for functions returning void (representing value-less "return;"), /// This is only valid for functions returning void (representing value-less "return;"),
/// and for iterators (representing "yield break;"). /// and for iterators (representing "yield break;").
///
/// Note: returns false for leave instructions that indirectly leave the function
/// (e.g. leaving a try block, and the try-finally construct is immediately followed
/// by another leave instruction)
/// </summary> /// </summary>
public bool IsLeavingFunction { public bool IsLeavingFunction {
get { return targetContainer?.Parent is ILFunction; } get { return targetContainer?.Parent is ILFunction; }

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

@ -20,7 +20,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text;
using ICSharpCode.Decompiler.IL.ControlFlow; using ICSharpCode.Decompiler.IL.ControlFlow;
using ICSharpCode.Decompiler.Util; using ICSharpCode.Decompiler.Util;
@ -39,18 +38,25 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{ {
this.context = context; this.context = context;
foreach (var loop in function.Descendants.OfType<BlockContainer>()) foreach (var block in function.Descendants.OfType<Block>())
{ {
if (loop.Kind != ContainerKind.Loop) for (int i = 0; i < block.Instructions.Count; i++)
continue;
if (MatchWhileLoop(loop, out var condition, out var loopBody))
{ {
if (context.Settings.ForStatement) var loop = block.Instructions[i] as BlockContainer;
MatchForLoop(loop, condition, loopBody); if (loop == null || loop.Kind != ContainerKind.Loop)
continue; continue;
// convert a "return" to a "break" so that we can match the high-level
// loop patterns
RemoveRedundantReturn.ReturnToBreak(block, loop, context);
if (MatchWhileLoop(loop, out var condition, out var loopBody))
{
if (context.Settings.ForStatement)
MatchForLoop(loop, condition, loopBody);
continue;
}
if (context.Settings.DoWhileStatement && MatchDoWhileLoop(loop))
continue;
} }
if (context.Settings.DoWhileStatement && MatchDoWhileLoop(loop))
continue;
} }
} }

10
ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs

@ -95,7 +95,7 @@ namespace ICSharpCode.Decompiler.IL
var inst = block.Instructions[i]; var inst = block.Instructions[i];
// the next instruction to be executed. Transformations will change the next instruction, so this is a method instead of a variable // the next instruction to be executed. Transformations will change the next instruction, so this is a method instead of a variable
ILInstruction NextInsn() => i + 1 < block.Instructions.Count ? block.Instructions[i + 1] : nextInstruction; ILInstruction NextInsn() => block.Instructions.ElementAtOrDefault(i + 1) ?? nextInstruction;
switch (inst) switch (inst)
{ {
@ -103,10 +103,14 @@ namespace ICSharpCode.Decompiler.IL
// visit the contents of the container // visit the contents of the container
Visit(container, continueTarget); Visit(container, continueTarget);
if (container.Kind == ContainerKind.Switch)
{
RemoveRedundantReturn.ReturnToBreak(block, container, context);
}
// reduce nesting in switch blocks // reduce nesting in switch blocks
if (container.Kind == ContainerKind.Switch && if (container.Kind == ContainerKind.Switch &&
CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit1) && CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit1) &&
ReduceSwitchNesting(block, container, keywordExit1)) ReduceSwitchNesting(block, container, keywordExit1))
{ {
RemoveRedundantExit(block, nextInstruction); RemoveRedundantExit(block, nextInstruction);
} }

2
ILSpy/LoadedAssembly.cs

@ -473,7 +473,7 @@ namespace ICSharpCode.ILSpy
return module; return module;
} }
string file = parent.GetUniversalResolver().FindAssemblyFile(reference); string? file = parent.GetUniversalResolver().FindAssemblyFile(reference);
if (file != null) if (file != null)
{ {

Loading…
Cancel
Save