diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index 150f540bd..eea0ebd0c 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -308,6 +308,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers if (flags.HasFlag(CompilerOptions.UseRoslyn2_10_0) || flags.HasFlag(CompilerOptions.UseRoslynLatest)) { + preprocessorSymbols.Add("ROSLYN2"); preprocessorSymbols.Add("CS70"); preprocessorSymbols.Add("CS71"); preprocessorSymbols.Add("CS72"); @@ -315,6 +316,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers } if (flags.HasFlag(CompilerOptions.UseRoslynLatest)) { + preprocessorSymbols.Add("ROSLYN3"); preprocessorSymbols.Add("CS73"); preprocessorSymbols.Add("CS80"); preprocessorSymbols.Add("VB16"); diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index d043cccf5..1e4e1bf2e 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -108,6 +108,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 30ee7b18d..e8984124b 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -304,6 +304,12 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(cscOptions: cscOptions); } + [Test] + public void PatternMatching([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions); + } + [Test] public void InitializerTests([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { @@ -480,7 +486,7 @@ namespace ICSharpCode.Decompiler.Tests [Test] public void Issue1080([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) { - RunForLibrary(cscOptions: cscOptions); + RunForLibrary(cscOptions: cscOptions, decompilerSettings: new DecompilerSettings(CSharp.LanguageVersion.CSharp6)); } [Test] diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs index 56f9c2729..4eacfacde 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs @@ -367,7 +367,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { Console.WriteLine(); } -#if ROSLYN +#if ROSLYN2 // Roslyn 2.9 started invoking op_Equality even if the source code says 'a != b' if (!(a == b)) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index cb884bf78..bdfc8cbfa 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -21,7 +21,7 @@ using System.Collections.Generic; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { - public class PatternMatching + public class OutVariables { public static void OutVarInShortCircuit(Dictionary d) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs new file mode 100644 index 000000000..064db32bc --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs @@ -0,0 +1,248 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public class PatternMatching + { + public void SimpleTypePattern(object x) + { + if (x is string value) + { + Console.WriteLine(value); + } + } + + public void TypePatternWithShortcircuit(object x) + { + Use(F() && x is string text && text.Contains("a")); + if (F() && x is string text2 && text2.Contains("a")) + { + Console.WriteLine(text2); + } + } + + public void TypePatternWithShortcircuitAnd(object x) + { + if (x is string text && text.Contains("a")) + { + Console.WriteLine(text); + } + else + { + Console.WriteLine(); + } + } + + public void TypePatternWithShortcircuitOr(object x) + { + if (!(x is string text) || text.Contains("a")) + { + Console.WriteLine(); + } + else + { + Console.WriteLine(text); + } + } + + public void TypePatternWithShortcircuitOr2(object x) + { + if (F() || !(x is string value)) + { + Console.WriteLine(); + } + else + { + Console.WriteLine(value); + } + } + + public void TypePatternValueTypesCondition(object x) + { + if (x is int num) + { + Console.WriteLine("Integer: " + num); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypesCondition2() + { + if (GetObject() is int num) + { + Console.WriteLine("Integer: " + num); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypesWithShortcircuitAnd(object x) + { + if (x is int num && num.GetHashCode() > 0) + { + Console.WriteLine("Positive integer: " + num); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypesWithShortcircuitOr(object x) + { + if (!(x is int value) || value.GetHashCode() > 0) + { + Console.WriteLine(); + } + else + { + Console.WriteLine(value); + } + } + +#if ROSLYN3 || OPT + // Roslyn 2.x generates a complex infeasible path in debug builds, which RemoveInfeasiblePathTransform + // currently cannot handle. Because this would increase the complexity of that transform, we ignore + // this case. + public void TypePatternValueTypesWithShortcircuitOr2(object x) + { + if (F() || !(x is int value)) + { + Console.WriteLine(); + } + else + { + Console.WriteLine(value); + } + } +#endif + + public void TypePatternGenerics(object x) + { + if (x is T val) + { + Console.WriteLine(val.GetType().FullName); + } + else + { + Console.WriteLine("not a " + typeof(T).FullName); + } + } + + public void TypePatternGenericRefType(object x) where T : class + { + if (x is T val) + { + Console.WriteLine(val.GetType().FullName); + } + else + { + Console.WriteLine("not a " + typeof(T).FullName); + } + } + + public void TypePatternGenericValType(object x) where T : struct + { + if (x is T val) + { + Console.WriteLine(val.GetType().FullName); + } + else + { + Console.WriteLine("not a " + typeof(T).FullName); + } + } + + public void TypePatternValueTypesWithShortcircuitAndMultiUse(object x) + { + if (x is int num && num.GetHashCode() > 0 && num % 2 == 0) + { + Console.WriteLine("Positive integer: " + num); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypesWithShortcircuitAndMultiUse2(object x) + { + if ((x is int num && num.GetHashCode() > 0 && num % 2 == 0) || F()) + { + Console.WriteLine("true"); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypesWithShortcircuitAndMultiUse3(object x) + { + if (F() || (x is int num && num.GetHashCode() > 0 && num % 2 == 0)) + { + Console.WriteLine("true"); + } + else + { + Console.WriteLine("else"); + } + } + + public void TypePatternValueTypes() + { + Use(F() && GetObject() is int num && num.GetHashCode() > 0 && num % 2 == 0); + } + + public static void NotTypePatternVariableUsedOutsideTrueBranch(object x) + { + string text = x as string; + if (text != null && text.Length > 5) + { + Console.WriteLine("pattern matches"); + } + if (text != null && text.Length > 10) + { + Console.WriteLine("other use!"); + } + } + + public static void NotTypePatternBecauseVarIsNotDefAssignedInCaseOfFallthrough(object x) + { +#if OPT + string obj = x as string; + if (obj == null) + { + Console.WriteLine("pattern doesn't match"); + } + Console.WriteLine(obj == null); +#else + string text = x as string; + if (text == null) + { + Console.WriteLine("pattern doesn't match"); + } + Console.WriteLine(text == null); +#endif + } + + private bool F() + { + return true; + } + + private object GetObject() + { + throw new NotImplementedException(); + } + + private void Use(bool x) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ShortCircuit.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ShortCircuit.cs index f753d91c3..4e7f7e3b1 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ShortCircuit.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ShortCircuit.cs @@ -320,15 +320,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty if (!F(1)) { } - if (F(2) && F(3)) - { - } if (F(4) || F(5)) { } - if (F(0) && F(1) && !F(2) && (F(3) || F(4))) - { - } E(); } #endif diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 93700561c..42144c5e6 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -89,6 +89,7 @@ namespace ICSharpCode.Decompiler.CSharp new SplitVariables(), new ILInlining(), new InlineReturnTransform(), // must run before DetectPinnedRegions + new RemoveInfeasiblePathTransform(), new DetectPinnedRegions(), // must run after inlining but before non-critical control flow transforms new YieldReturnDecompiler(), // must run after inlining but before loop detection new AsyncAwaitDecompiler(), // must run after inlining but before loop detection @@ -96,10 +97,10 @@ namespace ICSharpCode.Decompiler.CSharp new DetectExitPoints(), new LdLocaDupInitObjTransform(), new EarlyExpressionTransforms(), + new SplitVariables(), // split variables once again, because the stobj(ldloca V, ...) may open up new replacements // RemoveDeadVariableInit must run after EarlyExpressionTransforms so that stobj(ldloca V, ...) // is already collapsed into stloc(V, ...). new RemoveDeadVariableInit(), - new SplitVariables(), // split variables once again, because the stobj(ldloca V, ...) may open up new replacements new ControlFlowSimplification(), //split variables may enable new branch to leave inlining new DynamicCallSiteTransform(), new SwitchDetection(), @@ -119,6 +120,7 @@ namespace ICSharpCode.Decompiler.CSharp }, // re-run DetectExitPoints after loop detection new DetectExitPoints(), + new PatternMatchingTransform(), // must run after LoopDetection and before ConditionDetection new BlockILTransform { // per-block transforms PostOrderTransforms = { new ConditionDetection(), diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 94d67b7c0..264361f63 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4338,6 +4338,48 @@ namespace ICSharpCode.Decompiler.CSharp } } + protected internal override TranslatedExpression VisitMatchInstruction(MatchInstruction inst, TranslationContext context) + { + var left = Translate(inst.TestedOperand); + var right = TranslatePattern(inst); + + return new BinaryOperatorExpression(left, BinaryOperatorType.IsPattern, right) + .WithRR(new ResolveResult(compilation.FindType(KnownTypeCode.Boolean))) + .WithILInstruction(inst); + } + + ExpressionWithILInstruction TranslatePattern(ILInstruction pattern) + { + switch (pattern) + { + case MatchInstruction matchInstruction: + if (!matchInstruction.CheckType) + throw new NotImplementedException(); + if (matchInstruction.IsDeconstructCall) + throw new NotImplementedException(); + if (matchInstruction.IsDeconstructTuple) + throw new NotImplementedException(); + if (matchInstruction.SubPatterns.Any()) + throw new NotImplementedException(); + if (matchInstruction.HasDesignator) + { + SingleVariableDesignation designator = new SingleVariableDesignation { Identifier = matchInstruction.Variable.Name }; + designator.AddAnnotation(new ILVariableResolveResult(matchInstruction.Variable)); + return new DeclarationExpression { + Type = ConvertType(matchInstruction.Variable.Type), + Designation = designator + }.WithILInstruction(matchInstruction); + } + else + { + return new TypeReferenceExpression(ConvertType(matchInstruction.Variable.Type)) + .WithILInstruction(matchInstruction); + } + default: + throw new NotImplementedException(); + } + } + protected internal override TranslatedExpression VisitInvalidBranch(InvalidBranch inst, TranslationContext context) { string message = "Error"; diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 987677f7a..c7766b1e7 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -822,6 +822,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor spacePolicy = policy.SpaceAroundShiftOperator; break; case BinaryOperatorType.NullCoalescing: + case BinaryOperatorType.IsPattern: spacePolicy = true; break; case BinaryOperatorType.Range: @@ -831,7 +832,15 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor throw new NotSupportedException("Invalid value for BinaryOperatorType"); } Space(spacePolicy); - WriteToken(BinaryOperatorExpression.GetOperatorRole(binaryOperatorExpression.Operator)); + TokenRole tokenRole = BinaryOperatorExpression.GetOperatorRole(binaryOperatorExpression.Operator); + if (tokenRole == BinaryOperatorExpression.IsKeywordRole) + { + WriteKeyword(tokenRole); + } + else + { + WriteToken(tokenRole); + } Space(spacePolicy); binaryOperatorExpression.Right.AcceptVisitor(this); EndNode(binaryOperatorExpression); @@ -2788,7 +2797,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor public virtual void VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation) { StartNode(singleVariableDesignation); - writer.WriteIdentifier(singleVariableDesignation.IdentifierToken); + WriteIdentifier(singleVariableDesignation.IdentifierToken); EndNode(singleVariableDesignation); } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs index 4b706e127..e279cf021 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs @@ -143,6 +143,8 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor return PrecedenceLevel.ConditionalOr; case BinaryOperatorType.NullCoalescing: return PrecedenceLevel.NullCoalescing; + case BinaryOperatorType.IsPattern: + return PrecedenceLevel.RelationalAndTypeTesting; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs index 579d92e53..f64752e06 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs @@ -25,7 +25,6 @@ // THE SOFTWARE. using System; -using System.Collections.Generic; using System.Linq.Expressions; namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -55,6 +54,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public readonly static TokenRole ShiftRightRole = new TokenRole(">>"); public readonly static TokenRole NullCoalescingRole = new TokenRole("??"); public readonly static TokenRole RangeRole = new TokenRole(".."); + public readonly static TokenRole IsKeywordRole = IsExpression.IsKeywordRole; public readonly static Role LeftRole = new Role("Left", Expression.Null); public readonly static Role RightRole = new Role("Right", Expression.Null); @@ -155,6 +155,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return NullCoalescingRole; case BinaryOperatorType.Range: return RangeRole; + case BinaryOperatorType.IsPattern: + return IsKeywordRole; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } @@ -265,6 +267,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax NullCoalescing, /// left .. right /// left and right are optional = may be Expression.Null - Range + Range, + + /// left is right + /// right must be a pattern + IsPattern, } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index 5964caf42..510821104 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -355,6 +355,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms case VariableKind.ExceptionStackSlot: case VariableKind.UsingLocal: case VariableKind.ForeachLocal: + case VariableKind.PatternLocal: return false; default: return true; diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index b456b3203..fc3797092 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -104,6 +104,7 @@ namespace ICSharpCode.Decompiler discards = false; localFunctions = false; deconstruction = false; + patternMatching = false; } if (languageVersion < CSharp.LanguageVersion.CSharp7_2) { @@ -159,7 +160,7 @@ namespace ICSharpCode.Decompiler return CSharp.LanguageVersion.CSharp7_2; // C# 7.1 missing if (outVariables || throwExpressions || tupleTypes || tupleConversions - || discards || localFunctions) + || discards || localFunctions || deconstruction || patternMatching) return CSharp.LanguageVersion.CSharp7; if (awaitInCatchFinally || useExpressionBodyForCalculatedGetterOnlyProperties || nullPropagation || stringInterpolation || dictionaryInitializers || extensionMethodsInCollectionInitializers @@ -1466,6 +1467,24 @@ namespace ICSharpCode.Decompiler } } + bool patternMatching = true; + + /// + /// Gets/Sets whether C# 7.0 pattern matching should be detected. + /// + [Category("C# 7.0 / VS 2017")] + [Description("DecompilerSettings.PatternMatching")] + public bool PatternMatching { + get { return patternMatching; } + set { + if (patternMatching != value) + { + patternMatching = value; + OnPropertyChanged(); + } + } + } + bool staticLocalFunctions = true; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index fc8b7b9df..7ebd56f7c 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -98,6 +98,8 @@ + + diff --git a/ICSharpCode.Decompiler/IL/ILVariable.cs b/ICSharpCode.Decompiler/IL/ILVariable.cs index 07dee70ef..f70707a36 100644 --- a/ICSharpCode.Decompiler/IL/ILVariable.cs +++ b/ICSharpCode.Decompiler/IL/ILVariable.cs @@ -101,6 +101,7 @@ namespace ICSharpCode.Decompiler.IL case VariableKind.ExceptionLocal: case VariableKind.ForeachLocal: case VariableKind.UsingLocal: + case VariableKind.PatternLocal: case VariableKind.PinnedLocal: case VariableKind.PinnedRegionLocal: case VariableKind.DisplayClassLocal: @@ -173,6 +174,7 @@ namespace ICSharpCode.Decompiler.IL { case VariableKind.Local: case VariableKind.ForeachLocal: + case VariableKind.PatternLocal: case VariableKind.PinnedLocal: case VariableKind.PinnedRegionLocal: case VariableKind.UsingLocal: @@ -372,6 +374,11 @@ namespace ICSharpCode.Decompiler.IL /// public IField? StateMachineField; + /// + /// If enabled, remove dead stores to this variable as if the "Remove dead code" option is enabled. + /// + internal bool RemoveIfRedundant; + public ILVariable(VariableKind kind, IType type, int? index = null) { if (type == null) @@ -536,6 +543,8 @@ namespace ICSharpCode.Decompiler.IL return false; if (x.Kind == VariableKind.StackSlot || y.Kind == VariableKind.StackSlot) return false; + if (x.Kind == VariableKind.PatternLocal || y.Kind == VariableKind.PatternLocal) + return false; if (!(x.Function == y.Function && x.Kind == y.Kind)) return false; if (x.Index != null) diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs index e5833cd16..6948c79f3 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -84,7 +84,7 @@ namespace ICSharpCode.Decompiler.IL comp(deconstruct.result1(c) == 1), match.type[D].deconstruct(d = deconstruct.result2(c)) { comp(deconstruct.result1(d) == 2), - comp(deconstruct.result2(d) == 2), + comp(deconstruct.result2(d) == 3), } } */ @@ -126,10 +126,15 @@ namespace ICSharpCode.Decompiler.IL testedOperand = m.testedOperand; return true; case Comp comp: - testedOperand = comp.Left; - return IsConstant(comp.Right); - case ILInstruction logicNot when logicNot.MatchLogicNot(out var operand): - return IsPatternMatch(operand, out testedOperand); + if (comp.MatchLogicNot(out var operand)) + { + return IsPatternMatch(operand, out testedOperand); + } + else + { + testedOperand = comp.Left; + return IsConstant(comp.Right); + } default: testedOperand = null; return false; @@ -145,6 +150,7 @@ namespace ICSharpCode.Decompiler.IL OpCode.LdcI4 => true, OpCode.LdcI8 => true, OpCode.LdNull => true, + OpCode.LdStr => true, _ => false }; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs index a4fb74a3b..b0b4dfd02 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs @@ -40,6 +40,60 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + protected internal override void VisitComp(Comp inst) + { + base.VisitComp(inst); + FixComparisonKindLdNull(inst, context); + } + + internal static void FixComparisonKindLdNull(Comp inst, ILTransformContext context) + { + if (inst.IsLifted) + { + return; + } + if (inst.Right.MatchLdNull()) + { + if (inst.Kind == ComparisonKind.GreaterThan) + { + context.Step("comp(left > ldnull) => comp(left != ldnull)", inst); + inst.Kind = ComparisonKind.Inequality; + } + else if (inst.Kind == ComparisonKind.LessThanOrEqual) + { + context.Step("comp(left <= ldnull) => comp(left == ldnull)", inst); + inst.Kind = ComparisonKind.Equality; + } + } + else if (inst.Left.MatchLdNull()) + { + if (inst.Kind == ComparisonKind.LessThan) + { + context.Step("comp(ldnull < right) => comp(ldnull != right)", inst); + inst.Kind = ComparisonKind.Inequality; + } + else if (inst.Kind == ComparisonKind.GreaterThanOrEqual) + { + context.Step("comp(ldnull >= right) => comp(ldnull == right)", inst); + inst.Kind = ComparisonKind.Equality; + } + } + + if (inst.Right.MatchLdNull() && inst.Left.MatchBox(out var arg, out var type) && type.Kind == TypeKind.TypeParameter) + { + if (inst.Kind == ComparisonKind.Equality) + { + context.Step("comp(box T(..) == ldnull) -> comp(.. == ldnull)", inst); + inst.Left = arg; + } + if (inst.Kind == ComparisonKind.Inequality) + { + context.Step("comp(box T(..) != ldnull) -> comp(.. != ldnull)", inst); + inst.Left = arg; + } + } + } + protected internal override void VisitStObj(StObj inst) { base.VisitStObj(inst); diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 827b902a3..f9fa9a7b8 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -113,32 +113,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms { return; } - if (inst.Right.MatchLdNull()) - { - if (inst.Kind == ComparisonKind.GreaterThan) - { - context.Step("comp(left > ldnull) => comp(left != ldnull)", inst); - inst.Kind = ComparisonKind.Inequality; - } - else if (inst.Kind == ComparisonKind.LessThanOrEqual) - { - context.Step("comp(left <= ldnull) => comp(left == ldnull)", inst); - inst.Kind = ComparisonKind.Equality; - } - } - else if (inst.Left.MatchLdNull()) - { - if (inst.Kind == ComparisonKind.LessThan) - { - context.Step("comp(ldnull < right) => comp(ldnull != right)", inst); - inst.Kind = ComparisonKind.Inequality; - } - else if (inst.Kind == ComparisonKind.GreaterThanOrEqual) - { - context.Step("comp(ldnull >= right) => comp(ldnull == right)", inst); - inst.Kind = ComparisonKind.Equality; - } - } + EarlyExpressionTransforms.FixComparisonKindLdNull(inst, context); var rightWithoutConv = inst.Right.UnwrapConv(ConversionKind.SignExtend).UnwrapConv(ConversionKind.ZeroExtend); if (rightWithoutConv.MatchLdcI4(0) @@ -183,20 +158,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms inst.Right.AddILRange(rightWithoutConv); } } - - if (inst.Right.MatchLdNull() && inst.Left.MatchBox(out arg, out var type) && type.Kind == TypeKind.TypeParameter) - { - if (inst.Kind == ComparisonKind.Equality) - { - context.Step("comp(box T(..) == ldnull) -> comp(.. == ldnull)", inst); - inst.Left = arg; - } - if (inst.Kind == ComparisonKind.Inequality) - { - context.Step("comp(box T(..) != ldnull) -> comp(.. != ldnull)", inst); - inst.Left = arg; - } - } } protected internal override void VisitConv(Conv inst) @@ -589,11 +550,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms return; } } + if (MatchInstruction.IsPatternMatch(inst.Condition, out _) + && inst.TrueInst.MatchLdcI4(1) && inst.FalseInst.MatchLdcI4(0)) + { + context.Step("match(x) ? true : false -> match(x)", inst); + inst.Condition.AddILRange(inst); + inst.ReplaceWith(inst.Condition); + return; + } } IfInstruction HandleConditionalOperator(IfInstruction inst) { - // if (cond) stloc (A, V1) else stloc (A, V2) --> stloc (A, if (cond) V1 else V2) + // if (cond) stloc A(V1) else stloc A(V2) --> stloc A(if (cond) V1 else V2) Block trueInst = inst.TrueInst as Block; if (trueInst == null || trueInst.Instructions.Count != 1) return inst; diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index c06741edb..0db949e94 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -589,8 +589,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms //case OpCode.BinaryNumericInstruction when parent.SlotInfo == SwitchInstruction.ValueSlot: case OpCode.StringToInt when parent.SlotInfo == SwitchInstruction.ValueSlot: return true; - case OpCode.MatchInstruction when ((MatchInstruction)parent).IsDeconstructTuple: - return true; + case OpCode.MatchInstruction: + var match = (MatchInstruction)parent; + if (match.IsDeconstructTuple + || (match.CheckType && match.Variable.Type.IsReferenceType != true)) + { + return true; + } + break; } // decide based on the top-level target instruction into which we are inlining: switch (next.OpCode) diff --git a/ICSharpCode.Decompiler/IL/Transforms/IntroduceNativeIntTypeOnLocals.cs b/ICSharpCode.Decompiler/IL/Transforms/IntroduceNativeIntTypeOnLocals.cs index aa78ecedd..910a4726a 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IntroduceNativeIntTypeOnLocals.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IntroduceNativeIntTypeOnLocals.cs @@ -36,6 +36,7 @@ namespace ICSharpCode.Decompiler.IL { if (variable.Kind != VariableKind.Local && variable.Kind != VariableKind.StackSlot && + variable.Kind != VariableKind.PatternLocal && variable.Kind != VariableKind.ForeachLocal && variable.Kind != VariableKind.UsingLocal) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs index 45429d845..44f73e3e4 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs @@ -137,6 +137,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms } return false; } + + bool AnalyzeNegatedCondition(ILInstruction condition) + { + return condition.MatchLogicNot(out var arg) && AnalyzeCondition(arg); + } #endregion #region Main lifting logic @@ -211,11 +216,28 @@ namespace ICSharpCode.Decompiler.IL.Transforms // => comp.lifted[C#](lhs, rhs) return LiftCSharpComparison(comp, comp.Kind); } - else if (trueInst.MatchLdcI4(0) && AnalyzeCondition(falseInst)) + if (trueInst.MatchLdcI4(0) && AnalyzeCondition(falseInst)) { // comp(lhs, rhs) ? false : (v1 != null && ... && vn != null) return LiftCSharpComparison(comp, comp.Kind.Negate()); } + if (falseInst.MatchLdcI4(1) && AnalyzeNegatedCondition(trueInst)) + { + // comp(lhs, rhs) ? !(v1 != null && ... && vn != null) : true + // => !comp.lifted[C#](lhs, rhs) + ILInstruction result = LiftCSharpComparison(comp, comp.Kind); + if (result == null) + return result; + return Comp.LogicNot(result); + } + if (trueInst.MatchLdcI4(1) && AnalyzeNegatedCondition(falseInst)) + { + // comp(lhs, rhs) ? true : !(v1 != null && ... && vn != null) + ILInstruction result = LiftCSharpComparison(comp, comp.Kind.Negate()); + if (result == null) + return result; + return Comp.LogicNot(result); + } } } ILVariable v; @@ -544,7 +566,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; } - Call LiftCSharpUserEqualityComparison(CompOrDecimal hasValueComp, ComparisonKind newComparisonKind, ILInstruction nestedIfInst) + ILInstruction LiftCSharpUserEqualityComparison(CompOrDecimal hasValueComp, ComparisonKind newComparisonKind, ILInstruction nestedIfInst) { // User-defined equality operator: // if (comp(call get_HasValue(ldloca nullable1) == call get_HasValue(ldloca nullable2))) @@ -576,11 +598,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; if (!falseInst.MatchLdcI4(newComparisonKind == ComparisonKind.Equality ? 1 : 0)) return null; + bool trueInstNegated = false; + while (trueInst.MatchLogicNot(out var arg)) + { + trueInstNegated = !trueInstNegated; + trueInst = arg; + } if (!(trueInst is Call call)) return null; if (!(call.Method.IsOperator && call.Arguments.Count == 2)) return null; - if (call.Method.Name != (newComparisonKind == ComparisonKind.Equality ? "op_Equality" : "op_Inequality")) + bool expectEqualityOperator = (newComparisonKind == ComparisonKind.Equality) ^ trueInstNegated; + if (call.Method.Name != (expectEqualityOperator ? "op_Equality" : "op_Inequality")) return null; var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method); if (liftedOperator == null) @@ -594,13 +623,66 @@ namespace ICSharpCode.Decompiler.IL.Transforms ) { context.Step("NullableLiftingTransform: C# user-defined (in)equality comparison", nestedIfInst); - return new Call(liftedOperator) { + ILInstruction replacement = new Call(liftedOperator) { Arguments = { left, right }, ConstrainedTo = call.ConstrainedTo, ILStackWasEmpty = call.ILStackWasEmpty, IsTail = call.IsTail, }.WithILRange(call); + if (trueInstNegated) + { + replacement = Comp.LogicNot(replacement); + } + return replacement; + } + return null; + } + + ILInstruction LiftCSharpUserComparison(ILInstruction trueInst, ILInstruction falseInst) + { + // (v1 != null && ... && vn != null) ? trueInst : falseInst + bool trueInstNegated = false; + while (trueInst.MatchLogicNot(out var arg)) + { + trueInstNegated = !trueInstNegated; + trueInst = arg; + } + if (trueInst is Call call && !call.IsLifted + && CSharp.Resolver.CSharpOperators.IsComparisonOperator(call.Method) + && falseInst.MatchLdcI4((call.Method.Name == "op_Inequality") ^ trueInstNegated ? 1 : 0)) + { + // (v1 != null && ... && vn != null) ? call op_LessThan(lhs, rhs) : ldc.i4(0) + var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method); + if ((call.Method.Name == "op_Equality" || call.Method.Name == "op_Inequality") && nullableVars.Count != 1) + { + // Equality is special (returns true if both sides are null), only handle it + // in the normal code path if we're dealing with only a single nullable var + // (comparing nullable with non-nullable). + return null; + } + if (liftedOperator == null) + { + return null; + } + context.Step("Lift user-defined comparison operator", trueInst); + var (left, right, bits) = DoLiftBinary(call.Arguments[0], call.Arguments[1], + call.Method.Parameters[0].Type, call.Method.Parameters[1].Type); + if (left != null && right != null && bits.All(0, nullableVars.Count)) + { + ILInstruction result = new Call(liftedOperator) { + Arguments = { left, right }, + ConstrainedTo = call.ConstrainedTo, + ILStackWasEmpty = call.ILStackWasEmpty, + IsTail = call.IsTail + }.WithILRange(call); + if (trueInstNegated) + { + result = Comp.LogicNot(result); + } + return result; + } } + return null; } #endregion @@ -641,35 +723,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms UnderlyingResultType = NullableType.GetUnderlyingType(nullableVars[0].Type).GetStackType() }; } - else if (trueInst is Call call && !call.IsLifted - && CSharp.Resolver.CSharpOperators.IsComparisonOperator(call.Method) - && falseInst.MatchLdcI4(call.Method.Name == "op_Inequality" ? 1 : 0)) - { - // (v1 != null && ... && vn != null) ? call op_LessThan(lhs, rhs) : ldc.i4(0) - var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method); - if ((call.Method.Name == "op_Equality" || call.Method.Name == "op_Inequality") && nullableVars.Count != 1) - { - // Equality is special (returns true if both sides are null), only handle it - // in the normal code path if we're dealing with only a single nullable var - // (comparing nullable with non-nullable). - liftedOperator = null; - } - if (liftedOperator != null) - { - context.Step("Lift user-defined comparison operator", trueInst); - var (left, right, bits) = DoLiftBinary(call.Arguments[0], call.Arguments[1], - call.Method.Parameters[0].Type, call.Method.Parameters[1].Type); - if (left != null && right != null && bits.All(0, nullableVars.Count)) - { - return new Call(liftedOperator) { - Arguments = { left, right }, - ConstrainedTo = call.ConstrainedTo, - ILStackWasEmpty = call.ILStackWasEmpty, - IsTail = call.IsTail - }.WithILRange(call); - } - } - } + ILInstruction result = LiftCSharpUserComparison(trueInst, falseInst); + if (result != null) + return result; } ILInstruction lifted; if (nullableVars.Count == 1 && MatchGetValueOrDefault(exprToLift, nullableVars[0])) diff --git a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs new file mode 100644 index 000000000..6eb2863f5 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs @@ -0,0 +1,350 @@ +// Copyright (c) 2021 Daniel Grunwald, Siegfried Pammer +// +// 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; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using ICSharpCode.Decompiler.IL.ControlFlow; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + class PatternMatchingTransform : IILTransform + { + + void IILTransform.Run(ILFunction function, ILTransformContext context) + { + if (!context.Settings.PatternMatching) + return; + foreach (var container in function.Descendants.OfType()) + { + ControlFlowGraph? cfg = null; + foreach (var block in container.Blocks) + { + if (PatternMatchValueTypes(block, container, context, ref cfg)) + { + continue; + } + if (PatternMatchRefTypes(block, container, context, ref cfg)) + { + continue; + } + } + } + } + + /// Block { + /// ... + /// stloc V(isinst T(testedOperand)) + /// if (comp.o(ldloc V == ldnull)) br falseBlock + /// br trueBlock + /// } + /// + /// All other uses of V are in blocks dominated by trueBlock. + /// => + /// Block { + /// ... + /// if (match.type[T].notnull(V = testedOperand)) br trueBlock + /// br falseBlock + /// } + /// + /// - or - + /// + /// Block { + /// stloc s(isinst T(testedOperand)) + /// stloc v(ldloc s) + /// if (logic.not(comp.o(ldloc s != ldnull))) br falseBlock + /// br trueBlock + /// } + /// => + /// Block { + /// ... + /// if (match.type[T].notnull(V = testedOperand)) br trueBlock + /// br falseBlock + /// } + /// + /// All other uses of V are in blocks dominated by trueBlock. + private bool PatternMatchRefTypes(Block block, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg) + { + if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) + return false; + int pos = block.Instructions.Count - 3; + if (condition.MatchLdLoc(out var conditionVar)) + { + // stloc conditionVar(comp.o(ldloc s == ldnull)) + // if (logic.not(ldloc conditionVar)) br trueBlock + if (pos < 0) + return false; + if (!(conditionVar.IsSingleDefinition && conditionVar.LoadCount == 1 + && conditionVar.Kind == VariableKind.StackSlot)) + { + return false; + } + if (!block.Instructions[pos].MatchStLoc(conditionVar, out condition)) + return false; + pos--; + } + if (condition.MatchCompEqualsNull(out var loadInNullCheck)) + { + ExtensionMethods.Swap(ref trueInst, ref falseInst); + } + else if (condition.MatchCompNotEqualsNull(out loadInNullCheck)) + { + // do nothing + } + else + { + return false; + } + if (!loadInNullCheck.MatchLdLoc(out var s)) + return false; + if (!s.IsSingleDefinition) + return false; + if (s.Kind is not (VariableKind.Local or VariableKind.StackSlot)) + return false; + if (pos < 0) + return false; + // stloc V(isinst T(testedOperand)) + ILInstruction storeToV = block.Instructions[pos]; + if (!storeToV.MatchStLoc(out var v, out var value)) + return false; + if (value.MatchLdLoc(s)) + { + // stloc v(ldloc s) + pos--; + if (!block.Instructions[pos].MatchStLoc(s, out value)) + return false; + if (v.Kind is not (VariableKind.Local or VariableKind.StackSlot)) + return false; + if (s.LoadCount != 2) + return false; + } + else + { + if (v != s) + return false; + } + IType? unboxType; + if (value is UnboxAny unboxAny) + { + // stloc S(unbox.any T(isinst T(testedOperand))) + unboxType = unboxAny.Type; + value = unboxAny.Argument; + } + else + { + unboxType = null; + } + if (value is not IsInst { Argument: var testedOperand, Type: var type }) + return false; + if (type.IsReferenceType != true) + return false; + if (!(unboxType == null || type.Equals(unboxType))) + return false; + + if (!v.Type.Equals(type)) + return false; + if (!CheckAllUsesDominatedBy(v, container, trueInst, storeToV, loadInNullCheck, context, ref cfg)) + return false; + context.Step($"Type pattern matching {v.Name}", block); + // if (match.type[T].notnull(V = testedOperand)) br trueBlock + + var ifInst = (IfInstruction)block.Instructions.SecondToLastOrDefault()!; + + ifInst.Condition = new MatchInstruction(v, testedOperand) { + CheckNotNull = true, + CheckType = true + }.WithILRange(ifInst.Condition); + ifInst.TrueInst = trueInst; + block.Instructions[block.Instructions.Count - 1] = falseInst; + block.Instructions.RemoveRange(pos, ifInst.ChildIndex - pos); + v.Kind = VariableKind.PatternLocal; + return true; + } + + private bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILInstruction trueInst, + ILInstruction storeToV, ILInstruction? loadInNullCheck, ILTransformContext context, ref ControlFlowGraph? cfg) + { + var targetBlock = trueInst as Block; + if (targetBlock == null && !trueInst.MatchBranch(out targetBlock)) + { + return false; + } + + if (targetBlock.Parent != container) + return false; + if (targetBlock.IncomingEdgeCount != 1) + return false; + cfg ??= new ControlFlowGraph(container, context.CancellationToken); + var targetBlockNode = cfg.GetNode(targetBlock); + var uses = v.LoadInstructions.Concat(v.AddressInstructions) + .Concat(v.StoreInstructions.Cast()); + foreach (var use in uses) + { + if (use == storeToV || use == loadInNullCheck) + continue; + Block? found = null; + for (ILInstruction? current = use; current != null; current = current.Parent) + { + if (current.Parent == container) + { + found = (Block)current; + break; + } + } + if (found == null) + return false; + var node = cfg.GetNode(found); + if (!targetBlockNode.Dominates(node)) + return false; + } + return true; + } + + /// Block { + /// ... + /// [stloc temp(ldloc testedOperand)] + /// if (comp.o(isinst T(ldloc testedOperand) == ldnull)) br falseBlock + /// br unboxBlock + /// } + /// + /// Block unboxBlock (incoming: 1) { + /// stloc V(unbox.any T(ldloc temp)) + /// ... + /// } + /// => + /// Block { + /// ... + /// if (match.type[T].notnull(V = testedOperand)) br unboxBlock + /// br falseBlock + /// } + private bool PatternMatchValueTypes(Block block, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg) + { + if (!MatchIsInstBlock(block, out var type, out var testedOperand, + out var unboxBlock, out var falseBlock)) + { + return false; + } + StLoc? tempStore = block.Instructions.ElementAtOrDefault(block.Instructions.Count - 3) as StLoc; + if (tempStore == null || !tempStore.Value.MatchLdLoc(testedOperand.Variable)) + { + tempStore = null; + } + if (!MatchUnboxBlock(unboxBlock, type, out var unboxOperand, out var v, out var storeToV)) + { + return false; + } + if (unboxOperand == testedOperand.Variable) + { + // do nothing + } + else if (unboxOperand == tempStore?.Variable) + { + if (!(tempStore.Variable.IsSingleDefinition && tempStore.Variable.LoadCount == 1)) + return false; + } + else + { + return false; + } + if (!CheckAllUsesDominatedBy(v, container, unboxBlock, storeToV, null, context, ref cfg)) + return false; + context.Step($"PatternMatching with {v.Name}", block); + var ifInst = (IfInstruction)block.Instructions.SecondToLastOrDefault()!; + ifInst.Condition = new MatchInstruction(v, testedOperand) { + CheckNotNull = true, + CheckType = true + }; + ((Branch)ifInst.TrueInst).TargetBlock = unboxBlock; + ((Branch)block.Instructions.Last()).TargetBlock = falseBlock; + unboxBlock.Instructions.RemoveAt(0); + if (unboxOperand == tempStore?.Variable) + { + block.Instructions.Remove(tempStore); + } + // HACK: condition detection uses StartILOffset of blocks to decide which branch of if-else + // should become the then-branch. Change the unboxBlock StartILOffset from an offset inside + // the pattern matching machinery to an offset belonging to an instruction in the then-block. + unboxBlock.SetILRange(unboxBlock.Instructions[0]); + v.Kind = VariableKind.PatternLocal; + return true; + } + + /// ... + /// if (comp.o(isinst T(ldloc testedOperand) == ldnull)) br falseBlock + /// br unboxBlock + private bool MatchIsInstBlock(Block block, + [NotNullWhen(true)] out IType? type, + [NotNullWhen(true)] out LdLoc? testedOperand, + [NotNullWhen(true)] out Block? unboxBlock, + [NotNullWhen(true)] out Block? falseBlock) + { + type = null; + testedOperand = null; + unboxBlock = null; + falseBlock = null; + if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) + return false; + if (condition.MatchCompEqualsNull(out var arg)) + { + ExtensionMethods.Swap(ref trueInst, ref falseInst); + } + else if (condition.MatchCompNotEqualsNull(out arg)) + { + // do nothing + } + else + { + return false; + } + if (!arg.MatchIsInst(out arg, out type)) + return false; + testedOperand = arg as LdLoc; + if (testedOperand == null) + return false; + return trueInst.MatchBranch(out unboxBlock) && falseInst.MatchBranch(out falseBlock) + && unboxBlock.Parent == block.Parent && falseBlock.Parent == block.Parent; + } + + /// Block unboxBlock (incoming: 1) { + /// stloc V(unbox.any T(ldloc testedOperand)) + /// ... + /// } + private bool MatchUnboxBlock(Block unboxBlock, IType type, [NotNullWhen(true)] out ILVariable? testedOperand, + [NotNullWhen(true)] out ILVariable? v, [NotNullWhen(true)] out ILInstruction? storeToV) + { + v = null; + storeToV = null; + testedOperand = null; + if (unboxBlock.IncomingEdgeCount != 1) + return false; + storeToV = unboxBlock.Instructions[0]; + if (!storeToV.MatchStLoc(out v, out var value)) + return false; + if (!(value.MatchUnboxAny(out var arg, out var t) && t.Equals(type) && arg.MatchLdLoc(out testedOperand))) + return false; + + return true; + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs index 6a95e4c3d..284914cfa 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs @@ -42,41 +42,44 @@ namespace ICSharpCode.Decompiler.IL.Transforms // This is necessary to remove useless stores generated by some compilers, e.g., the F# compiler. // In yield return + async, the C# compiler tends to store null/default(T) to variables // when the variable goes out of scope. - if (function.IsAsync || function.IsIterator || context.Settings.RemoveDeadStores) + bool removeDeadStores = function.IsAsync || function.IsIterator || context.Settings.RemoveDeadStores; + + var variableQueue = new Queue(function.Variables); + while (variableQueue.Count > 0) { - var variableQueue = new Queue(function.Variables); - while (variableQueue.Count > 0) + var v = variableQueue.Dequeue(); + if (v.Kind != VariableKind.Local && v.Kind != VariableKind.StackSlot) + continue; + if (!(v.RemoveIfRedundant || removeDeadStores)) + continue; + // Skip variables that are captured in a mcs yield state-machine + // loads of these will only be visible after DelegateConstruction step. + if (function.StateMachineCompiledWithMono && v.StateMachineField != null) + continue; + if (v.LoadCount != 0 || v.AddressCount != 0) + continue; + foreach (var stloc in v.StoreInstructions.OfType().ToArray()) { - var v = variableQueue.Dequeue(); - if (v.Kind != VariableKind.Local && v.Kind != VariableKind.StackSlot) - continue; - // Skip variables that are captured in a mcs yield state-machine - // loads of these will only be visible after DelegateConstruction step. - if (function.StateMachineCompiledWithMono && v.StateMachineField != null) - continue; - if (v.LoadCount != 0 || v.AddressCount != 0) - continue; - foreach (var stloc in v.StoreInstructions.OfType().ToArray()) + if (stloc.Parent is Block block) { - if (stloc.Parent is Block block) + context.Step($"Dead store to {v.Name}", stloc); + if (SemanticHelper.IsPure(stloc.Value.Flags)) + { + block.Instructions.Remove(stloc); + } + else { - if (SemanticHelper.IsPure(stloc.Value.Flags)) - { - block.Instructions.Remove(stloc); - } - else - { - stloc.ReplaceWith(stloc.Value); - } - if (stloc.Value is LdLoc ldloc) - { - variableQueue.Enqueue(ldloc.Variable); - } + stloc.ReplaceWith(stloc.Value); + } + if (stloc.Value is LdLoc ldloc) + { + variableQueue.Enqueue(ldloc.Variable); } } } } + // Try to infer IType of stack slots that are of StackType.Ref: foreach (var v in function.Variables) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs new file mode 100644 index 000000000..3f4cbbefb --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs @@ -0,0 +1,117 @@ +// Copyright (c) 2021 Daniel Grunwald, Siegfried Pammer +// +// 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.CodeAnalysis; +using System.Linq; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + /// + /// Block IL_0018 (incoming: *) { + /// stloc s(ldc.i4 1) + /// br IL_0019 + /// } + /// + /// Block IL_0019 (incoming: > 1) { + /// if (logic.not(ldloc s)) br IL_0027 + /// br IL_001d + /// } + /// + /// replace br IL_0019 with br IL_0027 + /// + class RemoveInfeasiblePathTransform : IILTransform + { + void IILTransform.Run(ILFunction function, ILTransformContext context) + { + foreach (var container in function.Descendants.OfType()) + { + bool changed = false; + foreach (var block in container.Blocks) + { + changed |= DoTransform(block, context); + } + + if (changed) + { + container.SortBlocks(deleteUnreachableBlocks: true); + } + } + } + + + private bool DoTransform(Block block, ILTransformContext context) + { + if (!MatchBlock1(block, out var s, out int value, out var br)) + return false; + if (!MatchBlock2(br.TargetBlock, s, value, out var exitInst)) + return false; + context.Step("RemoveInfeasiblePath", br); + br.ReplaceWith(exitInst.Clone()); + s.RemoveIfRedundant = true; + return true; + } + + // Block IL_0018 (incoming: *) { + // stloc s(ldc.i4 1) + // br IL_0019 + // } + private bool MatchBlock1(Block block, [NotNullWhen(true)] out ILVariable? variable, out int constantValue, [NotNullWhen(true)] out Branch? branch) + { + variable = null; + constantValue = 0; + branch = null; + if (block.Instructions.Count != 2) + return false; + if (block.Instructions[0] is not StLoc + { + Variable: { Kind: VariableKind.StackSlot } s, + Value: LdcI4 { Value: 0 or 1 } valueInst + }) + { + return false; + } + if (block.Instructions[1] is not Branch br) + return false; + variable = s; + constantValue = valueInst.Value; + branch = br; + return true; + } + + // Block IL_0019 (incoming: > 1) { + // if (logic.not(ldloc s)) br IL_0027 + // br IL_001d + // } + bool MatchBlock2(Block block, ILVariable s, int constantValue, [NotNullWhen(true)] out ILInstruction? exitInst) + { + exitInst = null; + if (block.Instructions.Count != 2) + return false; + if (block.IncomingEdgeCount <= 1) + return false; + if (!block.MatchIfAtEndOfBlock(out var load, out var trueInst, out var falseInst)) + return false; + if (!load.MatchLdLoc(s)) + return false; + exitInst = constantValue != 0 ? trueInst : falseInst; + return exitInst is Branch or Leave { Value: Nop }; + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs index 621450a17..9ef159530 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs @@ -64,7 +64,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms case VariableKind.StackSlot: // stack slots: are already split by construction, // except for the locals-turned-stackslots in async functions - if (v.Function.IsAsync) + // or stack slots handled by the infeasible path transform + if (v.Function.IsAsync || v.RemoveIfRedundant) goto case VariableKind.Local; else return false; @@ -270,6 +271,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms v.HasGeneratedName = inst.Variable.HasGeneratedName; v.StateMachineField = inst.Variable.StateMachineField; v.HasInitialValue = false; // we'll set HasInitialValue when we encounter an uninit load + v.RemoveIfRedundant = inst.Variable.RemoveIfRedundant; newVariables.Add(representative, v); inst.Variable.Function.Variables.Add(v); } diff --git a/ICSharpCode.Decompiler/IL/Transforms/StatementTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/StatementTransform.cs index d19643ca6..51911595d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/StatementTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/StatementTransform.cs @@ -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.Diagnostics; diff --git a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs index 53795a1d3..e59257f78 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs @@ -37,12 +37,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms this.context = context; for (int i = block.Instructions.Count - 1; i >= 0; i--) { - if (!TransformUsing(block, i) && !TransformUsingVB(block, i) && !TransformAsyncUsing(block, i)) + if (TransformUsing(block, i)) + { + continue; + } + if (TransformUsingVB(block, i)) + { + continue; + } + if (TransformAsyncUsing(block, i)) + { continue; - // This happens in some cases: - // Use correct index after transformation. - if (i >= block.Instructions.Count) - i = block.Instructions.Count; + } } } @@ -74,9 +80,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// bool TransformUsing(Block block, int i) { - if (i < 1) + if (i + 1 >= block.Instructions.Count) return false; - if (!(block.Instructions[i] is TryFinally tryFinally) || !(block.Instructions[i - 1] is StLoc storeInst)) + if (!(block.Instructions[i + 1] is TryFinally tryFinally) || !(block.Instructions[i] is StLoc storeInst)) return false; if (!(storeInst.Value.MatchLdNull() || CheckResourceType(storeInst.Variable.Type))) return false; @@ -88,12 +94,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (storeInst.Variable.StoreInstructions.Count > 1) return false; - if (!(tryFinally.FinallyBlock is BlockContainer container) || !MatchDisposeBlock(container, storeInst.Variable, storeInst.Value.MatchLdNull())) + if (!(tryFinally.FinallyBlock is BlockContainer container)) + return false; + if (!MatchDisposeBlock(container, storeInst.Variable, storeInst.Value.MatchLdNull())) return false; context.Step("UsingTransform", tryFinally); storeInst.Variable.Kind = VariableKind.UsingLocal; - block.Instructions.RemoveAt(i); - block.Instructions[i - 1] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { + block.Instructions.RemoveAt(i + 1); + block.Instructions[i] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsRefStruct = context.Settings.IntroduceRefModifiersOnStructs && storeInst.Variable.Type.Kind == TypeKind.Struct && storeInst.Variable.Type.IsByRefLike }.WithILRange(storeInst); return true; @@ -127,6 +135,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// bool TransformUsingVB(Block block, int i) { + if (i >= block.Instructions.Count) + return false; if (!(block.Instructions[i] is TryFinally tryFinally)) return false; if (!(tryFinally.TryBlock is BlockContainer tryContainer && tryContainer.EntryPoint.Instructions.FirstOrDefault() is StLoc storeInst)) @@ -175,41 +185,64 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - bool MatchDisposeBlock(BlockContainer container, ILVariable objVar, bool usingNull, in string disposeMethodFullName = "System.IDisposable.Dispose", KnownTypeCode disposeTypeCode = KnownTypeCode.IDisposable) + /// finally BlockContainer { + /// Block IL_0012(incoming: 1) { + /// if (comp(ldloc obj != ldnull)) Block IL_001a { + /// callvirt Dispose(obj) + /// } + /// leave IL_0012(nop) + /// } + /// } + bool MatchDisposeBlock(BlockContainer container, ILVariable objVar, bool usingNull, + in string disposeMethodFullName = "System.IDisposable.Dispose", + KnownTypeCode disposeTypeCode = KnownTypeCode.IDisposable) { var entryPoint = container.EntryPoint; - if (entryPoint.Instructions.Count < 2 || entryPoint.Instructions.Count > 3 || entryPoint.IncomingEdgeCount != 1) + if (entryPoint.IncomingEdgeCount != 1) return false; - int leaveIndex = entryPoint.Instructions.Count == 2 ? 1 : 2; - int checkIndex = entryPoint.Instructions.Count == 2 ? 0 : 1; - int castIndex = entryPoint.Instructions.Count == 3 ? 0 : -1; - var checkInst = entryPoint.Instructions[checkIndex]; + int pos = 0; bool isReference = objVar.Type.IsReferenceType != false; - if (castIndex > -1) + // optional: + // stloc temp(isinst TDisposable(ldloc obj)) + if (entryPoint.Instructions.ElementAtOrDefault(pos).MatchStLoc(out var tempVar, out var isinst)) { - if (!entryPoint.Instructions[castIndex].MatchStLoc(out var tempVar, out var isinst)) - return false; - if (!isinst.MatchIsInst(out var load, out var disposableType) || !load.MatchLdLoc(objVar) || !disposableType.IsKnownType(disposeTypeCode)) + if (!isinst.MatchIsInst(out var load, out var disposableType) || !load.MatchLdLoc(objVar) + || !disposableType.IsKnownType(disposeTypeCode)) + { return false; + } if (!tempVar.IsSingleDefinition) return false; isReference = true; - if (!MatchDisposeCheck(tempVar, checkInst, isReference, usingNull, out int numObjVarLoadsInCheck, disposeMethodFullName, disposeTypeCode)) - return false; - if (tempVar.LoadCount != numObjVarLoadsInCheck) - return false; + pos++; + objVar = tempVar; } - else + // if (comp(ldloc obj != ldnull)) Block IL_001a { + // callvirt Dispose(obj) + // } + var checkInst = entryPoint.Instructions.ElementAtOrDefault(pos); + if (checkInst == null) + return false; + if (!MatchDisposeCheck(objVar, checkInst, isReference, usingNull, + out int numObjVarLoadsInCheck, disposeMethodFullName, disposeTypeCode)) { - if (!MatchDisposeCheck(objVar, checkInst, isReference, usingNull, out _, disposeMethodFullName, disposeTypeCode)) - return false; + return false; } - if (!entryPoint.Instructions[leaveIndex].MatchLeave(container, out var returnValue) || !returnValue.MatchNop()) + // make sure the (optional) temporary is used only in the dispose check + if (pos > 0 && objVar.LoadCount != numObjVarLoadsInCheck) return false; - return true; + pos++; + // make sure, the finally ends in a leave(nop) instruction. + if (!entryPoint.Instructions.ElementAtOrDefault(pos).MatchLeave(container, out var returnValue)) + return false; + if (!returnValue.MatchNop()) + return false; + // leave is the last instruction + return (pos + 1) == entryPoint.Instructions.Count; } - bool MatchDisposeCheck(ILVariable objVar, ILInstruction checkInst, bool isReference, bool usingNull, out int numObjVarLoadsInCheck, string disposeMethodFullName, KnownTypeCode disposeTypeCode) + bool MatchDisposeCheck(ILVariable objVar, ILInstruction checkInst, bool isReference, bool usingNull, + out int numObjVarLoadsInCheck, string disposeMethodFullName, KnownTypeCode disposeTypeCode) { numObjVarLoadsInCheck = 2; ILInstruction disposeInvocation; @@ -283,7 +316,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms // reference types have a null check. if (!checkInst.MatchIfInstruction(out var condition, out var disposeInst)) return false; - if (!condition.MatchCompNotEquals(out var left, out var right) || !left.MatchLdLoc(objVar) || !right.MatchLdNull()) + if (!MatchNullCheckOrTypeCheck(condition, ref objVar, disposeTypeCode)) return false; if (!(disposeInst is Block disposeBlock) || disposeBlock.Instructions.Count != 1) return false; @@ -348,6 +381,32 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + bool MatchNullCheckOrTypeCheck(ILInstruction condition, ref ILVariable objVar, KnownTypeCode disposeType) + { + if (condition.MatchCompNotEquals(out var left, out var right)) + { + if (!left.MatchLdLoc(objVar) || !right.MatchLdNull()) + return false; + return true; + } + if (condition is MatchInstruction + { + CheckNotNull: true, + CheckType: true, + TestedOperand: LdLoc { Variable: var v }, + Variable: var newObjVar + }) + { + if (v != objVar) + return false; + if (!newObjVar.Type.IsKnownType(disposeType)) + return false; + objVar = newObjVar; + return true; + } + return false; + } + /// /// stloc test(resourceExpression) /// .try BlockContainer { @@ -371,7 +430,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// private bool TransformAsyncUsing(Block block, int i) { - if (i < 1 || !context.Settings.AsyncUsingAndForEachStatement) + if (!context.Settings.AsyncUsingAndForEachStatement) + return false; + if (i < 1 || i >= block.Instructions.Count) return false; if (!(block.Instructions[i] is TryFinally tryFinally) || !(block.Instructions[i - 1] is StLoc storeInst)) return false;