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 @@ -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
// 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
@ -476,4 +498,4 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -476,4 +498,4 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
#endif
}
}
}

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

@ -198,17 +198,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -198,17 +198,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
case 1:
Console.WriteLine("case 1");
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
@ -348,12 +346,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -348,12 +346,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine();
if (B(1))
if (!B(1))
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
return;
}
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
}
catch

3
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

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

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

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

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

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

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

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

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

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

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

@ -83,11 +83,15 @@ namespace ICSharpCode.Decompiler.IL @@ -83,11 +83,15 @@ namespace ICSharpCode.Decompiler.IL
}
/// <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).
///
/// This is only valid for functions returning void (representing value-less "return;"),
/// 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>
public bool IsLeavingFunction {
get { return targetContainer?.Parent is ILFunction; }

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

@ -20,7 +20,6 @@ using System; @@ -20,7 +20,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using ICSharpCode.Decompiler.IL.ControlFlow;
using ICSharpCode.Decompiler.Util;
@ -39,18 +38,25 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -39,18 +38,25 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
this.context = context;
foreach (var loop in function.Descendants.OfType<BlockContainer>())
foreach (var block in function.Descendants.OfType<Block>())
{
if (loop.Kind != ContainerKind.Loop)
continue;
if (MatchWhileLoop(loop, out var condition, out var loopBody))
for (int i = 0; i < block.Instructions.Count; i++)
{
if (context.Settings.ForStatement)
MatchForLoop(loop, condition, loopBody);
continue;
var loop = block.Instructions[i] as BlockContainer;
if (loop == null || loop.Kind != ContainerKind.Loop)
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 @@ -95,7 +95,7 @@ namespace ICSharpCode.Decompiler.IL
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
ILInstruction NextInsn() => i + 1 < block.Instructions.Count ? block.Instructions[i + 1] : nextInstruction;
ILInstruction NextInsn() => block.Instructions.ElementAtOrDefault(i + 1) ?? nextInstruction;
switch (inst)
{
@ -103,10 +103,14 @@ namespace ICSharpCode.Decompiler.IL @@ -103,10 +103,14 @@ namespace ICSharpCode.Decompiler.IL
// visit the contents of the container
Visit(container, continueTarget);
if (container.Kind == ContainerKind.Switch)
{
RemoveRedundantReturn.ReturnToBreak(block, container, context);
}
// reduce nesting in switch blocks
if (container.Kind == ContainerKind.Switch &&
CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit1) &&
ReduceSwitchNesting(block, container, keywordExit1))
CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit1) &&
ReduceSwitchNesting(block, container, keywordExit1))
{
RemoveRedundantExit(block, nextInstruction);
}

2
ILSpy/LoadedAssembly.cs

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

Loading…
Cancel
Save