diff --git a/.github/workflows/build-ilspy.yml b/.github/workflows/build-ilspy.yml index 2b6b3ed2cc..89291c79b8 100644 --- a/.github/workflows/build-ilspy.yml +++ b/.github/workflows/build-ilspy.yml @@ -95,7 +95,7 @@ jobs: run: | msbuild ILSpy.Installer.sln /t:Restore /p:Configuration="Release" /p:Platform="Any CPU" msbuild ILSpy.Installer.sln /p:Configuration="Release" /p:Platform="Any CPU" - msbuild ILSpy.Installer.sln /p:Configuration="Release" /p:Platform="Any CPU" /p:DefineConstants="ARM64" + msbuild ILSpy.Installer.sln /p:Configuration="Release" /p:Platform="Any CPU" /p:PlatformForInstaller="ARM64" - name: Build VS Extensions (for 2017-2019 and 2022) if: matrix.configuration == 'release' diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs index 5b9f38787b..165e4e4d6f 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs @@ -4,6 +4,35 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { public class PatternMatching { + public class X + { + public int? NullableIntField; + public S? NullableCustomStructField; + public int I { get; set; } + public string Text { get; set; } + public object Obj { get; set; } + public S CustomStruct { get; set; } + public int? NullableIntProp { get; set; } + public S? NullableCustomStructProp { get; set; } + } + + public struct S + { + public int I; + public string Text { get; set; } + public object Obj { get; set; } + public S2 S2 { get; set; } + } + + public struct S2 + { + public int I; + public float F; + public decimal D; + public string Text { get; set; } + public object Obj { get; set; } + } + public void SimpleTypePattern(object x) { if (x is string value) @@ -303,6 +332,427 @@ public void GenericValueTypePatternStringRequiresCastToObject(T x) where T : } } +#if CS80 + public void RecursivePattern_Type(object x) + { + if (x is X { Obj: string obj }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_TypeAndConst(object x) + { + if (x is X { Obj: string obj, I: 42 }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_Constant(object obj) + { + if (obj is X { Obj: null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_StringConstant(object obj) + { + if (obj is X { Text: "Hello" } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_MultipleConstants(object obj) + { + if (obj is X { I: 42, Text: "Hello" } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_ValueTypeWithField(object obj) + { + if (obj is S { I: 42 } s) + { + Console.WriteLine("Test " + s); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_MultipleConstantsMixedWithVar(object x) + { + if (x is X { I: 42, Obj: var obj, Text: "Hello" }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NonTypePattern(object obj) + { + if (obj is X { I: 42, Text: { Length: 0 } } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePatternValueType_NonTypePatternTwoProps(object obj) + { + if (obj is X { I: 42, CustomStruct: { I: 0, Text: "Test" } } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NonTypePatternNotNull(object o) + { + if (o is X { I: 42, Text: not null, Obj: var obj } x) + { + Console.WriteLine("Test " + x.I + " " + obj.GetType()); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_VarLengthPattern(object obj) + { + if (obj is X { I: 42, Text: { Length: var length } } x) + { + Console.WriteLine("Test " + x.I + ": " + length); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePatternValueType_VarLengthPattern(object obj) + { + if (obj is S { I: 42, Text: { Length: var length } } s) + { + Console.WriteLine("Test " + s.I + ": " + length); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePatternValueType_VarLengthPattern_SwappedProps(object obj) + { + if (obj is S { Text: { Length: var length }, I: 42 } s) + { + Console.WriteLine("Test " + s.I + ": " + length); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_VarLengthPattern_SwappedProps(object obj) + { + if (obj is X { Text: { Length: var length }, I: 42 } x) + { + Console.WriteLine("Test " + x.I + ": " + length); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntField_Const(object obj) + { + if (obj is X { NullableIntField: 42 } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntField_Null(object obj) + { + if (obj is X { NullableIntField: null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntField_NotNull(object obj) + { + if (obj is X { NullableIntField: not null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntField_Var(object obj) + { + if (obj is X { NullableIntField: var nullableIntField }) + { + Console.WriteLine("Test " + nullableIntField.Value); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntProp_Const(object obj) + { + if (obj is X { NullableIntProp: 42 } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntProp_Null(object obj) + { + if (obj is X { NullableIntProp: null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntProp_NotNull(object obj) + { + if (obj is X { NullableIntProp: not null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableIntProp_Var(object obj) + { + if (obj is X { NullableIntProp: var nullableIntProp }) + { + Console.WriteLine("Test " + nullableIntProp.Value); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructField_Const(object obj) + { + if (obj is X { NullableCustomStructField: { I: 42, Obj: not null } nullableCustomStructField }) + { + Console.WriteLine("Test " + nullableCustomStructField.I); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructField_Null(object obj) + { + if (obj is X { NullableCustomStructField: null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructField_NotNull(object obj) + { + if (obj is X { NullableCustomStructField: not null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructField_Var(object obj) + { + if (obj is X { NullableCustomStructField: var nullableCustomStructField, Obj: null }) + { + Console.WriteLine("Test " + nullableCustomStructField.Value); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructProp_Const(object obj) + { + if (obj is X { NullableCustomStructProp: { I: 42, Obj: not null } nullableCustomStructProp }) + { + Console.WriteLine("Test " + nullableCustomStructProp.Text); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructProp_Null(object obj) + { + if (obj is X { NullableCustomStructProp: null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructProp_NotNull(object obj) + { + if (obj is X { NullableCustomStructProp: not null } x) + { + Console.WriteLine("Test " + x); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_NullableCustomStructProp_Var(object obj) + { + if (obj is X { NullableCustomStructProp: var nullableCustomStructProp, Obj: null }) + { + Console.WriteLine("Test " + nullableCustomStructProp.Value); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_CustomStructNested_Null(object obj) + { + if (obj is S { S2: { Obj: null } }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_CustomStructNested_TextLengthZero(object obj) + { + if (obj is S { S2: { Text: { Length: 0 } } }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_CustomStructNested_EmptyString(object obj) + { + if (obj is S { S2: { Text: "" } }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_CustomStructNested_Float(object obj) + { + if (obj is S { S2: { F: 3.141f, Obj: null } }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } + + public void RecursivePattern_CustomStructNested_Decimal(object obj) + { + if (obj is S { S2: { D: 3.141m, Obj: null } }) + { + Console.WriteLine("Test " + obj); + } + else + { + Console.WriteLine("not Test"); + } + } +#endif private bool F() { return true; diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 707377b47d..b03f38b4ae 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4546,40 +4546,121 @@ protected internal override TranslatedExpression VisitMatchInstruction(MatchInst { left = left.UnwrapChild(castExpr.Expression); } - var right = TranslatePattern(inst); + var right = TranslatePattern(inst, left.Type); return new BinaryOperatorExpression(left, BinaryOperatorType.IsPattern, right) .WithRR(new ResolveResult(compilation.FindType(KnownTypeCode.Boolean))) .WithILInstruction(inst); } - ExpressionWithILInstruction TranslatePattern(ILInstruction pattern) + ExpressionWithILInstruction TranslatePattern(ILInstruction pattern, IType leftHandType) { 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) + if (matchInstruction.SubPatterns.Count > 0 || (matchInstruction.CheckNotNull && !matchInstruction.CheckType)) + { + RecursivePatternExpression recursivePatternExpression = new(); + if (matchInstruction.CheckType) + { + recursivePatternExpression.Type = ConvertType(matchInstruction.Variable.Type); + } + else + { + Debug.Assert(matchInstruction.CheckNotNull || matchInstruction.Variable.Type.IsReferenceType == false); + } + foreach (var subPattern in matchInstruction.SubPatterns) + { + if (!MatchInstruction.IsPatternMatch(subPattern, out var testedOperand, settings)) + { + Debug.Fail("Invalid sub pattern"); + continue; + } + IMember member; + if (testedOperand is CallInstruction call) + { + member = call.Method.AccessorOwner; + } + else if (testedOperand.MatchLdFld(out _, out var f)) + { + member = f; + } + else + { + Debug.Fail("Invalid sub pattern"); + continue; + } + recursivePatternExpression.SubPatterns.Add( + new NamedArgumentExpression { Name = member.Name, Expression = TranslatePattern(subPattern, member.ReturnType) } + .WithRR(new MemberResolveResult(null, member)) + ); + } + if (matchInstruction.HasDesignator) + { + SingleVariableDesignation designator = new SingleVariableDesignation { Identifier = matchInstruction.Variable.Name }; + designator.AddAnnotation(new ILVariableResolveResult(matchInstruction.Variable)); + recursivePatternExpression.Designation = designator; + } + return recursivePatternExpression.WithILInstruction(matchInstruction); + } + else if (matchInstruction.HasDesignator || !matchInstruction.CheckType) { SingleVariableDesignation designator = new SingleVariableDesignation { Identifier = matchInstruction.Variable.Name }; designator.AddAnnotation(new ILVariableResolveResult(matchInstruction.Variable)); + AstType type; + if (matchInstruction.CheckType) + { + type = ConvertType(matchInstruction.Variable.Type); + } + else + { + Debug.Assert(matchInstruction.IsVar); + type = new SimpleType("var"); + } return new DeclarationExpression { - Type = ConvertType(matchInstruction.Variable.Type), + Type = type, Designation = designator }.WithILInstruction(matchInstruction); } else { + Debug.Assert(matchInstruction.CheckType); return new TypeReferenceExpression(ConvertType(matchInstruction.Variable.Type)) .WithILInstruction(matchInstruction); } + case Comp comp: + var constantValue = Translate(comp.Right, leftHandType); + switch (comp.Kind) + { + case ComparisonKind.Equality: + return constantValue + .WithILInstruction(comp); + case ComparisonKind.Inequality: + return new UnaryOperatorExpression(UnaryOperatorType.PatternNot, constantValue) + .WithILInstruction(comp); + case ComparisonKind.LessThan: + return new UnaryOperatorExpression(UnaryOperatorType.PatternRelationalLessThan, constantValue) + .WithILInstruction(comp); + case ComparisonKind.LessThanOrEqual: + return new UnaryOperatorExpression(UnaryOperatorType.PatternRelationalLessThanOrEqual, constantValue) + .WithILInstruction(comp); + case ComparisonKind.GreaterThan: + return new UnaryOperatorExpression(UnaryOperatorType.PatternRelationalGreaterThan, constantValue) + .WithILInstruction(comp); + case ComparisonKind.GreaterThanOrEqual: + return new UnaryOperatorExpression(UnaryOperatorType.PatternRelationalGreaterThanOrEqual, constantValue) + .WithILInstruction(comp); + default: + throw new InvalidOperationException("Unexpected comparison kind: " + comp.Kind); + } + case Call call when MatchInstruction.IsCallToOpEquality(call, KnownTypeCode.String): + return Translate(call.Arguments[1]); + case Call call when MatchInstruction.IsCallToOpEquality(call, KnownTypeCode.Decimal): + return Translate(call.Arguments[1]); default: throw new NotImplementedException(); } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 3e428e5efb..7fee876ca0 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -948,6 +948,40 @@ public virtual void VisitDeclarationExpression(DeclarationExpression declaration EndNode(declarationExpression); } + public virtual void VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression) + { + StartNode(recursivePatternExpression); + + recursivePatternExpression.Type.AcceptVisitor(this); + Space(); + if (recursivePatternExpression.IsPositional) + { + WriteToken(Roles.LPar); + } + else + { + WriteToken(Roles.LBrace); + } + Space(); + WriteCommaSeparatedList(recursivePatternExpression.SubPatterns); + Space(); + if (recursivePatternExpression.IsPositional) + { + WriteToken(Roles.RPar); + } + else + { + WriteToken(Roles.RBrace); + } + if (!recursivePatternExpression.Designation.IsNull) + { + Space(); + recursivePatternExpression.Designation.AcceptVisitor(this); + } + + EndNode(recursivePatternExpression); + } + public virtual void VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { StartNode(outVarDeclarationExpression); @@ -1268,7 +1302,7 @@ public virtual void VisitUnaryOperatorExpression(UnaryOperatorExpression unaryOp StartNode(unaryOperatorExpression); UnaryOperatorType opType = unaryOperatorExpression.Operator; var opSymbol = UnaryOperatorExpression.GetOperatorRole(opType); - if (opType == UnaryOperatorType.Await) + if (opType is UnaryOperatorType.Await or UnaryOperatorType.PatternNot) { WriteKeyword(opSymbol); Space(); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index 41870ba48b..2016b429c1 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -512,6 +512,11 @@ public virtual void VisitDeclarationExpression(DeclarationExpression declaration VisitChildren(declarationExpression); } + public virtual void VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression) + { + VisitChildren(recursivePatternExpression); + } + public virtual void VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { VisitChildren(outVarDeclarationExpression); @@ -1190,6 +1195,11 @@ public virtual T VisitDeclarationExpression(DeclarationExpression declarationExp return VisitChildren(declarationExpression); } + public virtual T VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression) + { + return VisitChildren(recursivePatternExpression); + } + public virtual T VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { return VisitChildren(outVarDeclarationExpression); @@ -1868,6 +1878,11 @@ public virtual S VisitDeclarationExpression(DeclarationExpression declarationExp return VisitChildren(declarationExpression, data); } + public virtual S VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression, T data) + { + return VisitChildren(recursivePatternExpression, data); + } + public virtual S VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression, T data) { return VisitChildren(outVarDeclarationExpression, data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs index 0e85852332..5584565249 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs @@ -52,7 +52,9 @@ public override S AcceptVisitor(IAstVisitor visitor, T data) protected internal override bool DoMatch(AstNode other, Match match) { - return other is DeclarationExpression o && Designation.DoMatch(o.Designation, match); + return other is DeclarationExpression o + && Type.DoMatch(o.Type, match) + && Designation.DoMatch(o.Designation, match); } } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/RecursivePatternExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/RecursivePatternExpression.cs new file mode 100644 index 0000000000..3a26d735ba --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/RecursivePatternExpression.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2023 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. + +using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + public class RecursivePatternExpression : Expression + { + public static readonly Role SubPatternRole = new Role("SubPattern", Syntax.Expression.Null); + + public AstType Type { + get { return GetChildByRole(Roles.Type); } + set { SetChildByRole(Roles.Type, value); } + } + + public AstNodeCollection SubPatterns { + get { return GetChildrenByRole(SubPatternRole); } + } + + public VariableDesignation Designation { + get { return GetChildByRole(Roles.VariableDesignationRole); } + set { SetChildByRole(Roles.VariableDesignationRole, value); } + } + + public bool IsPositional { get; set; } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitRecursivePatternExpression(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitRecursivePatternExpression(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitRecursivePatternExpression(this, data); + } + + protected internal override bool DoMatch(AstNode other, Match match) + { + return other is RecursivePatternExpression o + && Type.DoMatch(o.Type, match) + && IsPositional == o.IsPositional + && SubPatterns.DoMatch(o.SubPatterns, match) + && Designation.DoMatch(o.Designation, match); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs index af925fc141..460f9a4217 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs @@ -46,6 +46,7 @@ public class UnaryOperatorExpression : Expression public readonly static TokenRole NullConditionalRole = new TokenRole("?"); public readonly static TokenRole SuppressNullableWarningRole = new TokenRole("!"); public readonly static TokenRole IndexFromEndRole = new TokenRole("^"); + public readonly static TokenRole PatternNotRole = new TokenRole("not"); public UnaryOperatorExpression() { @@ -126,6 +127,16 @@ public static TokenRole GetOperatorRole(UnaryOperatorType op) return SuppressNullableWarningRole; case UnaryOperatorType.IndexFromEnd: return IndexFromEndRole; + case UnaryOperatorType.PatternNot: + return PatternNotRole; + case UnaryOperatorType.PatternRelationalLessThan: + return BinaryOperatorExpression.LessThanRole; + case UnaryOperatorType.PatternRelationalLessThanOrEqual: + return BinaryOperatorExpression.LessThanOrEqualRole; + case UnaryOperatorType.PatternRelationalGreaterThan: + return BinaryOperatorExpression.GreaterThanRole; + case UnaryOperatorType.PatternRelationalGreaterThanOrEqual: + return BinaryOperatorExpression.GreaterThanOrEqualRole; default: throw new NotSupportedException("Invalid value for UnaryOperatorType"); } @@ -156,6 +167,11 @@ public static ExpressionType GetLinqNodeType(UnaryOperatorType op, bool checkFor case UnaryOperatorType.Await: case UnaryOperatorType.SuppressNullableWarning: case UnaryOperatorType.IndexFromEnd: + case UnaryOperatorType.PatternNot: + case UnaryOperatorType.PatternRelationalLessThan: + case UnaryOperatorType.PatternRelationalLessThanOrEqual: + case UnaryOperatorType.PatternRelationalGreaterThan: + case UnaryOperatorType.PatternRelationalGreaterThanOrEqual: return ExpressionType.Extension; default: throw new NotSupportedException("Invalid value for UnaryOperatorType"); @@ -216,5 +232,25 @@ public enum UnaryOperatorType /// C# 8 prefix ^ operator /// IndexFromEnd, + /// + /// C# 9 not pattern + /// + PatternNot, + /// + /// C# 9 relational pattern + /// + PatternRelationalLessThan, + /// + /// C# 9 relational pattern + /// + PatternRelationalLessThanOrEqual, + /// + /// C# 9 relational pattern + /// + PatternRelationalGreaterThan, + /// + /// C# 9 relational pattern + /// + PatternRelationalGreaterThanOrEqual, } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs index 25fa54d02e..37dfd40a33 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs @@ -36,6 +36,7 @@ public interface IAstVisitor void VisitCheckedExpression(CheckedExpression checkedExpression); void VisitConditionalExpression(ConditionalExpression conditionalExpression); void VisitDeclarationExpression(DeclarationExpression declarationExpression); + void VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression); void VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression); void VisitDirectionExpression(DirectionExpression directionExpression); void VisitIdentifierExpression(IdentifierExpression identifierExpression); @@ -184,6 +185,7 @@ public interface IAstVisitor S VisitCheckedExpression(CheckedExpression checkedExpression); S VisitConditionalExpression(ConditionalExpression conditionalExpression); S VisitDeclarationExpression(DeclarationExpression declarationExpression); + S VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression); S VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression); S VisitDirectionExpression(DirectionExpression directionExpression); S VisitIdentifierExpression(IdentifierExpression identifierExpression); @@ -332,6 +334,7 @@ public interface IAstVisitor S VisitCheckedExpression(CheckedExpression checkedExpression, T data); S VisitConditionalExpression(ConditionalExpression conditionalExpression, T data); S VisitDeclarationExpression(DeclarationExpression declarationExpression, T data); + S VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression, T data); S VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression, T data); S VisitDirectionExpression(DirectionExpression directionExpression, T data); S VisitIdentifierExpression(IdentifierExpression identifierExpression, T data); diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index ea0a7dde9b..c395390001 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -130,6 +130,7 @@ public void SetLanguageVersion(CSharp.LanguageVersion languageVersion) staticLocalFunctions = false; ranges = false; switchExpressions = false; + recursivePatternMatching = false; } if (languageVersion < CSharp.LanguageVersion.CSharp9_0) { @@ -141,6 +142,8 @@ public void SetLanguageVersion(CSharp.LanguageVersion languageVersion) withExpressions = false; usePrimaryConstructorSyntax = false; covariantReturns = false; + relationalPatterns = false; + patternCombinators = false; } if (languageVersion < CSharp.LanguageVersion.CSharp10_0) { @@ -166,10 +169,11 @@ public CSharp.LanguageVersion GetMinimumRequiredVersion() if (fileScopedNamespaces || recordStructs) return CSharp.LanguageVersion.CSharp10_0; if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension - || recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns) + || recordClasses || withExpressions || usePrimaryConstructorSyntax || covariantReturns + || relationalPatterns || patternCombinators) return CSharp.LanguageVersion.CSharp9_0; if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement - || staticLocalFunctions || ranges || switchExpressions) + || staticLocalFunctions || ranges || switchExpressions || recursivePatternMatching) return CSharp.LanguageVersion.CSharp8_0; if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers || patternBasedFixedStatement) @@ -1678,6 +1682,60 @@ public bool PatternMatching { } } + bool recursivePatternMatching = true; + + /// + /// Gets/Sets whether C# 8.0 recursive patterns should be detected. + /// + [Category("C# 8.0 / VS 2019")] + [Description("DecompilerSettings.RecursivePatternMatching")] + public bool RecursivePatternMatching { + get { return recursivePatternMatching; } + set { + if (recursivePatternMatching != value) + { + recursivePatternMatching = value; + OnPropertyChanged(); + } + } + } + + bool patternCombinators = true; + + /// + /// Gets/Sets whether C# 9.0 and, or, not patterns should be detected. + /// + [Category("C# 9.0 / VS 2019.8")] + [Description("DecompilerSettings.PatternCombinators")] + public bool PatternCombinators { + get { return patternCombinators; } + set { + if (patternCombinators != value) + { + patternCombinators = value; + OnPropertyChanged(); + } + } + } + + bool relationalPatterns = true; + + /// + /// Gets/Sets whether C# 9.0 relational patterns should be detected. + /// + [Category("C# 9.0 / VS 2019.8")] + [Description("DecompilerSettings.RelationalPatterns")] + public bool RelationalPatterns { + get { return relationalPatterns; } + set { + if (relationalPatterns != value) + { + relationalPatterns = value; + OnPropertyChanged(); + } + } + } + bool staticLocalFunctions = true; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 5ee433d8a1..3f8c724926 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -27,7 +27,7 @@ False false - 10 + 11 true True ICSharpCode.Decompiler.snk @@ -91,6 +91,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs index 227eef95ce..406d3847e1 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs @@ -130,7 +130,7 @@ public ComparisonKind Kind { } } - public readonly ComparisonLiftingKind LiftingKind; + public ComparisonLiftingKind LiftingKind; /// /// Gets the stack type of the comparison inputs. diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs index f12f06cf63..af9074f123 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -22,6 +22,7 @@ using System.Diagnostics.CodeAnalysis; using ICSharpCode.Decompiler.TypeSystem; + namespace ICSharpCode.Decompiler.IL { partial class MatchInstruction : ILInstruction @@ -118,7 +119,7 @@ public MatchInstruction(ILVariable variable, ILInstruction testedOperand) /// (even if the pattern fails to match!). /// The pattern matching instruction evaluates to 1 (as I4) if the pattern matches, or 0 otherwise. /// - public static bool IsPatternMatch(ILInstruction? inst, [NotNullWhen(true)] out ILInstruction? testedOperand) + public static bool IsPatternMatch(ILInstruction? inst, [NotNullWhen(true)] out ILInstruction? testedOperand, DecompilerSettings? settings) { switch (inst) { @@ -126,22 +127,45 @@ public static bool IsPatternMatch(ILInstruction? inst, [NotNullWhen(true)] out I testedOperand = m.testedOperand; return true; case Comp comp: - if (comp.MatchLogicNot(out var operand)) + if (comp.MatchLogicNot(out var operand) && IsPatternMatch(operand, out testedOperand, settings)) { - return IsPatternMatch(operand, out testedOperand); + return settings?.PatternCombinators ?? true; } else { testedOperand = comp.Left; + if (!(settings?.RelationalPatterns ?? true)) + { + if (comp.Kind is not (ComparisonKind.Equality or ComparisonKind.Inequality)) + return false; + } + if (!(settings?.PatternCombinators ?? true)) + { + if (comp.Kind is ComparisonKind.Inequality) + return false; + } return IsConstant(comp.Right); } + case Call call when IsCallToOpEquality(call, KnownTypeCode.String): + testedOperand = call.Arguments[0]; + return call.Arguments[1].OpCode == OpCode.LdStr; + case Call call when IsCallToOpEquality(call, KnownTypeCode.Decimal): + testedOperand = call.Arguments[0]; + return call.Arguments[1].OpCode == OpCode.LdcDecimal; default: testedOperand = null; return false; } } - private static bool IsConstant(ILInstruction inst) + internal static bool IsCallToOpEquality(Call call, KnownTypeCode knownType) + { + return call.Method.IsOperator && call.Method.Name == "op_Equality" + && call.Method.DeclaringType.IsKnownType(knownType) + && call.Arguments.Count == 2; + } + + internal static bool IsConstant(ILInstruction inst) { return inst.OpCode switch { OpCode.LdcDecimal => true, @@ -150,7 +174,6 @@ private static bool IsConstant(ILInstruction inst) OpCode.LdcI4 => true, OpCode.LdcI8 => true, OpCode.LdNull => true, - OpCode.LdStr => true, _ => false }; } @@ -191,7 +214,7 @@ void AdditionalInvariants() Debug.Assert(SubPatterns.Count >= NumPositionalPatterns); foreach (var subPattern in SubPatterns) { - if (!IsPatternMatch(subPattern, out ILInstruction? operand)) + if (!IsPatternMatch(subPattern, out ILInstruction? operand, null)) throw new InvalidOperationException("Sub-Pattern must be a valid pattern"); // the first child is TestedOperand int subPatternIndex = subPattern.ChildIndex - 1; @@ -202,12 +225,12 @@ void AdditionalInvariants() } else if (operand.MatchLdFld(out var target, out _)) { - Debug.Assert(target.MatchLdLoc(variable)); + Debug.Assert(target.MatchLdLocRef(variable)); } else if (operand is CallInstruction call) { Debug.Assert(call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter); - Debug.Assert(call.Arguments[0].MatchLdLoc(variable)); + Debug.Assert(call.Arguments[0].MatchLdLocRef(variable)); } else { diff --git a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs index e4331c74be..2fd3f7782d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs @@ -432,12 +432,25 @@ string GenerateNameForVariable(ILVariable variable) } if (string.IsNullOrEmpty(proposedName)) { - var proposedNameForStores = variable.StoreInstructions.OfType() - .Select(expr => GetNameFromInstruction(expr.Value)) - .Except(currentLowerCaseTypeOrMemberNames).ToList(); + var proposedNameForStores = new HashSet(); + foreach (var store in variable.StoreInstructions) + { + if (store is StLoc stloc) + { + var name = GetNameFromInstruction(stloc.Value); + if (!currentLowerCaseTypeOrMemberNames.Contains(name)) + proposedNameForStores.Add(name); + } + else if (store is MatchInstruction match && match.SlotInfo == MatchInstruction.SubPatternsSlot) + { + var name = GetNameFromInstruction(match.TestedOperand); + if (!currentLowerCaseTypeOrMemberNames.Contains(name)) + proposedNameForStores.Add(name); + } + } if (proposedNameForStores.Count == 1) { - proposedName = proposedNameForStores[0]; + proposedName = proposedNameForStores.Single(); } } if (string.IsNullOrEmpty(proposedName)) diff --git a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs index 12daf764c4..3d99d04009 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs @@ -172,5 +172,55 @@ internal static void AddressOfLdLocToLdLoca(LdObj inst, ILTransformContext conte temp.ReplaceWith(replacement); } } + + protected internal override void VisitNewObj(NewObj inst) + { + if (TransformDecimalCtorToConstant(inst, out LdcDecimal decimalConstant)) + { + context.Step("TransformDecimalCtorToConstant", inst); + inst.ReplaceWith(decimalConstant); + return; + } + + base.VisitNewObj(inst); + } + + bool TransformDecimalCtorToConstant(NewObj inst, out LdcDecimal result) + { + IType t = inst.Method.DeclaringType; + result = null; + if (!t.IsKnownType(KnownTypeCode.Decimal)) + return false; + var args = inst.Arguments; + if (args.Count == 1) + { + long val; + if (args[0].MatchLdcI(out val)) + { + var paramType = inst.Method.Parameters[0].Type.GetDefinition()?.KnownTypeCode; + result = paramType switch { + KnownTypeCode.Int32 => new LdcDecimal(new decimal(unchecked((int)val))), + KnownTypeCode.UInt32 => new LdcDecimal(new decimal(unchecked((uint)val))), + KnownTypeCode.Int64 => new LdcDecimal(new decimal(val)), + KnownTypeCode.UInt64 => new LdcDecimal(new decimal(unchecked((ulong)val))), + _ => null + }; + return result is not null; + } + } + else if (args.Count == 5) + { + int lo, mid, hi, isNegative, scale; + if (args[0].MatchLdcI4(out lo) && args[1].MatchLdcI4(out mid) && + args[2].MatchLdcI4(out hi) && args[3].MatchLdcI4(out isNegative) && + args[4].MatchLdcI4(out scale)) + { + result = new LdcDecimal(new decimal(lo, mid, hi, isNegative != 0, (byte)scale)); + return true; + } + } + return false; + } + } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 8a6684c4e0..7f494cad6b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -286,12 +286,6 @@ protected internal override void VisitCallVirt(CallVirt inst) protected internal override void VisitNewObj(NewObj inst) { - if (TransformDecimalCtorToConstant(inst, out LdcDecimal decimalConstant)) - { - context.Step("TransformDecimalCtorToConstant", inst); - inst.ReplaceWith(decimalConstant); - return; - } Block block; if (TransformSpanTCtorContainingStackAlloc(inst, out ILInstruction locallocSpan)) { @@ -419,43 +413,6 @@ bool MatchesElementCount(ILInstruction sizeInBytesInstr, IType elementType, ILIn return true; } - bool TransformDecimalCtorToConstant(NewObj inst, out LdcDecimal result) - { - IType t = inst.Method.DeclaringType; - result = null; - if (!t.IsKnownType(KnownTypeCode.Decimal)) - return false; - var args = inst.Arguments; - if (args.Count == 1) - { - long val; - if (args[0].MatchLdcI(out val)) - { - var paramType = inst.Method.Parameters[0].Type.GetDefinition()?.KnownTypeCode; - result = paramType switch { - KnownTypeCode.Int32 => new LdcDecimal(new decimal(unchecked((int)val))), - KnownTypeCode.UInt32 => new LdcDecimal(new decimal(unchecked((uint)val))), - KnownTypeCode.Int64 => new LdcDecimal(new decimal(val)), - KnownTypeCode.UInt64 => new LdcDecimal(new decimal(unchecked((ulong)val))), - _ => null - }; - return result is not null; - } - } - else if (args.Count == 5) - { - int lo, mid, hi, isNegative, scale; - if (args[0].MatchLdcI4(out lo) && args[1].MatchLdcI4(out mid) && - args[2].MatchLdcI4(out hi) && args[3].MatchLdcI4(out isNegative) && - args[4].MatchLdcI4(out scale)) - { - result = new LdcDecimal(new decimal(lo, mid, hi, isNegative != 0, (byte)scale)); - return true; - } - } - return false; - } - bool TransformDecimalFieldToConstant(LdObj inst, out LdcDecimal result) { if (inst.MatchLdsFld(out var field) && field.DeclaringType.IsKnownType(KnownTypeCode.Decimal)) @@ -557,7 +514,7 @@ protected internal override void VisitIfInstruction(IfInstruction inst) return; } } - if (MatchInstruction.IsPatternMatch(inst.Condition, out _) + if (MatchInstruction.IsPatternMatch(inst.Condition, out _, context.Settings) && inst.TrueInst.MatchLdcI4(1) && inst.FalseInst.MatchLdcI4(0)) { context.Step("match(x) ? true : false -> match(x)", inst); diff --git a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs index 9a02d9c7e3..d1bf695671 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs @@ -19,9 +19,9 @@ #nullable enable using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using ICSharpCode.Decompiler.IL.ControlFlow; using ICSharpCode.Decompiler.TypeSystem; @@ -39,7 +39,7 @@ void IILTransform.Run(ILFunction function, ILTransformContext context) foreach (var container in function.Descendants.OfType()) { ControlFlowGraph? cfg = null; - foreach (var block in container.Blocks) + foreach (var block in container.Blocks.Reverse()) { if (PatternMatchValueTypes(block, container, context, ref cfg)) { @@ -50,6 +50,7 @@ void IILTransform.Run(ILFunction function, ILTransformContext context) continue; } } + container.Blocks.RemoveAll(b => b.Instructions.Count == 0); } } @@ -179,10 +180,352 @@ private bool PatternMatchRefTypes(Block block, BlockContainer container, ILTrans block.Instructions[block.Instructions.Count - 1] = falseInst; block.Instructions.RemoveRange(pos, ifInst.ChildIndex - pos); v.Kind = VariableKind.PatternLocal; + + DetectPropertySubPatterns((MatchInstruction)ifInst.Condition, trueInst, falseInst, container, context, ref cfg); + return true; } - private bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILInstruction trueInst, + private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPattern, ILInstruction trueInst, + ILInstruction parentFalseInst, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg) + { + if (!context.Settings.RecursivePatternMatching) + { + return trueInst; + } + while (true) + { + Block? trueBlock = trueInst as Block; + if (!(trueBlock != null || trueInst.MatchBranch(out trueBlock))) + { + break; + } + if (!(trueBlock.IncomingEdgeCount == 1 && trueBlock.Parent == container)) + { + break; + } + var nextTrueInst = DetectPropertySubPattern(parentPattern, trueBlock, parentFalseInst, context, ref cfg); + if (nextTrueInst != null) + { + trueInst = nextTrueInst; + } + else + { + break; + } + } + return trueInst; + } + + private static ILInstruction? DetectPropertySubPattern(MatchInstruction parentPattern, Block block, + ILInstruction parentFalseInst, ILTransformContext context, ref ControlFlowGraph? cfg) + { + // if (match.notnull.type[System.String] (V_0 = callvirt get_C(ldloc V_2))) br IL_0022 + // br IL_0037 + if (MatchBlockContainingOneCondition(block, out var condition, out var trueInst, out var falseInst)) + { + bool negate = false; + if (!DetectExitPoints.CompatibleExitInstruction(parentFalseInst, falseInst)) + { + if (!DetectExitPoints.CompatibleExitInstruction(parentFalseInst, trueInst)) + { + return null; + } + ExtensionMethods.Swap(ref trueInst, ref falseInst); + negate = true; + } + if (MatchInstruction.IsPatternMatch(condition, out var operand, context.Settings)) + { + if (!PropertyOrFieldAccess(operand, out var target, out _)) + { + return null; + } + if (!target.MatchLdLocRef(parentPattern.Variable)) + { + return null; + } + if (negate && !context.Settings.PatternCombinators) + { + return null; + } + context.Step("Move property sub pattern", condition); + if (negate) + { + condition = Comp.LogicNot(condition); + } + parentPattern.SubPatterns.Add(condition); + } + else if (PropertyOrFieldAccess(condition, out var target, out _)) + { + if (!target.MatchLdLocRef(parentPattern.Variable)) + { + return null; + } + if (!negate && !context.Settings.PatternCombinators) + { + return null; + } + context.Step("Sub pattern: implicit != 0", condition); + parentPattern.SubPatterns.Add(new Comp(negate ? ComparisonKind.Equality : ComparisonKind.Inequality, + Sign.None, condition, new LdcI4(0))); + } + else + { + return null; + } + block.Instructions.Clear(); + block.Instructions.Add(trueInst); + return trueInst; + } + else if (block.Instructions[0].MatchStLoc(out var targetVariable, out var operand)) + { + if (!PropertyOrFieldAccess(operand, out var target, out var member)) + { + return null; + } + if (!target.MatchLdLocRef(parentPattern.Variable)) + { + return null; + } + if (!targetVariable.Type.Equals(member.ReturnType)) + { + return null; + } + if (!CheckAllUsesDominatedBy(targetVariable, (BlockContainer)block.Parent!, block, block.Instructions[0], null, context, ref cfg)) + { + return null; + } + context.Step("Property var pattern", block); + var varPattern = new MatchInstruction(targetVariable, operand) + .WithILRange(block.Instructions[0]); + parentPattern.SubPatterns.Add(varPattern); + block.Instructions.RemoveAt(0); + targetVariable.Kind = VariableKind.PatternLocal; + + if (targetVariable.Type.IsKnownType(KnownTypeCode.NullableOfT)) + { + return MatchNullableHasValueCheckPattern(block, varPattern, parentFalseInst, context, ref cfg) + ?? block; + } + + var instructionAfterNullCheck = MatchNullCheckPattern(block, varPattern, parentFalseInst, context); + if (instructionAfterNullCheck != null) + { + return DetectPropertySubPatterns(varPattern, instructionAfterNullCheck, parentFalseInst, (BlockContainer)block.Parent!, context, ref cfg); + } + else if (targetVariable.Type.IsReferenceType == false) + { + return DetectPropertySubPatterns(varPattern, block, parentFalseInst, (BlockContainer)block.Parent!, context, ref cfg); + } + else + { + return block; + } + } + else + { + return null; + } + } + + private static ILInstruction? MatchNullCheckPattern(Block block, MatchInstruction varPattern, + ILInstruction parentFalseInst, ILTransformContext context) + { + if (!MatchBlockContainingOneCondition(block, out var condition, out var trueInst, out var falseInst)) + { + return null; + } + if (condition.MatchCompEqualsNull(out var arg) && arg.MatchLdLoc(varPattern.Variable)) + { + ExtensionMethods.Swap(ref trueInst, ref falseInst); + } + else if (condition.MatchCompNotEqualsNull(out arg) && arg.MatchLdLoc(varPattern.Variable)) + { + } + else + { + return null; + } + if (!DetectExitPoints.CompatibleExitInstruction(falseInst, parentFalseInst)) + { + return null; + } + context.Step("Null check pattern", block); + varPattern.CheckNotNull = true; + block.Instructions.Clear(); + block.Instructions.Add(trueInst); + return trueInst; + } + + private static ILInstruction? MatchNullableHasValueCheckPattern(Block block, MatchInstruction varPattern, + ILInstruction parentFalseInst, ILTransformContext context, ref ControlFlowGraph? cfg) + { + if (!(varPattern.Variable.StoreCount == 1 && varPattern.Variable.LoadCount == 0)) + { + return null; + } + if (!MatchBlockContainingOneCondition(block, out var condition, out var trueInst, out var falseInst)) + { + return null; + } + if (!NullableLiftingTransform.MatchHasValueCall(condition, varPattern.Variable)) + { + return null; + } + if (!DetectExitPoints.CompatibleExitInstruction(falseInst, parentFalseInst)) + { + if (DetectExitPoints.CompatibleExitInstruction(trueInst, parentFalseInst)) + { + if (!(varPattern.Variable.AddressCount == 1)) + { + return null; + } + + context.Step("Nullable.HasValue check -> null pattern", block); + varPattern.ReplaceWith(new Comp(ComparisonKind.Equality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull())); + block.Instructions.Clear(); + block.Instructions.Add(falseInst); + return falseInst; + } + return null; + } + if (varPattern.Variable.AddressCount == 1 && context.Settings.PatternCombinators) + { + context.Step("Nullable.HasValue check -> not null pattern", block); + varPattern.ReplaceWith(new Comp(ComparisonKind.Inequality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull())); + block.Instructions.Clear(); + block.Instructions.Add(trueInst); + return trueInst; + } + else if (varPattern.Variable.AddressCount != 2) + { + return null; + } + if (!(trueInst.MatchBranch(out var trueBlock) && trueBlock.Parent == block.Parent && trueBlock.IncomingEdgeCount == 1)) + { + return null; + } + if (trueBlock.Instructions[0].MatchStLoc(out var newTargetVariable, out var getValueOrDefaultCall) + && NullableLiftingTransform.MatchGetValueOrDefault(getValueOrDefaultCall, varPattern.Variable)) + { + context.Step("Nullable.HasValue check + Nullable.GetValueOrDefault pattern", block); + varPattern.CheckNotNull = true; + varPattern.Variable = newTargetVariable; + newTargetVariable.Kind = VariableKind.PatternLocal; + block.Instructions.Clear(); + block.Instructions.Add(trueInst); + trueBlock.Instructions.RemoveAt(0); + return DetectPropertySubPatterns(varPattern, trueBlock, parentFalseInst, (BlockContainer)block.Parent!, context, ref cfg); + } + else if (MatchBlockContainingOneCondition(trueBlock, out condition, out trueInst, out falseInst)) + { + if (!(condition is Comp comp + && MatchInstruction.IsConstant(comp.Right) + && NullableLiftingTransform.MatchGetValueOrDefault(comp.Left, varPattern.Variable))) + { + return null; + } + if (!(context.Settings.RelationalPatterns || comp.Kind is ComparisonKind.Equality or ComparisonKind.Inequality)) + { + return null; + } + bool negated = false; + if (!DetectExitPoints.CompatibleExitInstruction(falseInst, parentFalseInst)) + { + if (!DetectExitPoints.CompatibleExitInstruction(trueInst, parentFalseInst)) + { + return null; + } + ExtensionMethods.Swap(ref trueInst, ref falseInst); + negated = true; + } + if (comp.Kind == (negated ? ComparisonKind.Equality : ComparisonKind.Inequality)) + { + return null; + } + if (negated && !context.Settings.PatternCombinators) + { + return null; + } + context.Step("Nullable.HasValue check + Nullable.GetValueOrDefault pattern", block); + // varPattern: match (v = testedOperand) + // comp: comp.i4(call GetValueOrDefault(ldloca v) != ldc.i4 42) + // => + // comp.i4.lifted(testedOperand != ldc.i4 42) + block.Instructions.Clear(); + block.Instructions.Add(trueInst); + trueBlock.Instructions.Clear(); + comp.Left = varPattern.TestedOperand; + comp.LiftingKind = ComparisonLiftingKind.CSharp; + if (negated) + { + comp = Comp.LogicNot(comp); + } + varPattern.ReplaceWith(comp); + return trueInst; + } + else + { + return null; + } + } + + private static bool PropertyOrFieldAccess(ILInstruction operand, [NotNullWhen(true)] out ILInstruction? target, [NotNullWhen(true)] out IMember? member) + { + if (operand is CallInstruction { + Method: { + SymbolKind: SymbolKind.Accessor, + AccessorKind: MethodSemanticsAttributes.Getter, + AccessorOwner: { } _member + }, + Arguments: [var _target] + }) + { + target = _target; + member = _member; + return true; + } + else if (operand.MatchLdFld(out target, out var field)) + { + member = field; + return true; + } + else + { + member = null; + return false; + } + } + + private static bool MatchBlockContainingOneCondition(Block block, [NotNullWhen(true)] out ILInstruction? condition, [NotNullWhen(true)] out ILInstruction? trueInst, [NotNullWhen(true)] out ILInstruction? falseInst) + { + switch (block.Instructions.Count) + { + case 2: + return block.MatchIfAtEndOfBlock(out condition, out trueInst, out falseInst); + case 3: + condition = null; + if (!block.MatchIfAtEndOfBlock(out var loadTemp, out trueInst, out falseInst)) + return false; + if (!(loadTemp.MatchLdLoc(out var tempVar) && tempVar.IsSingleDefinition && tempVar.LoadCount == 1)) + return false; + if (!block.Instructions[0].MatchStLoc(tempVar, out condition)) + return false; + while (condition.MatchLogicNot(out var arg)) + { + condition = arg; + ExtensionMethods.Swap(ref trueInst, ref falseInst); + } + return true; + default: + condition = null; + trueInst = null; + falseInst = null; + return false; + } + } + + private static bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILInstruction trueInst, ILInstruction storeToV, ILInstruction? loadInNullCheck, ILTransformContext context, ref ControlFlowGraph? cfg) { var targetBlock = trueInst as Block; @@ -241,7 +584,7 @@ private bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILI private bool PatternMatchValueTypes(Block block, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg) { if (!MatchIsInstBlock(block, out var type, out var testedOperand, out var testedVariable, - out var boxType1, out var unboxBlock, out var falseBlock)) + out var boxType1, out var unboxBlock, out var falseInst)) { return false; } @@ -279,8 +622,8 @@ private bool PatternMatchValueTypes(Block block, BlockContainer container, ILTra CheckNotNull = true, CheckType = true }; - ((Branch)ifInst.TrueInst).TargetBlock = unboxBlock; - ((Branch)block.Instructions.Last()).TargetBlock = falseBlock; + ifInst.TrueInst = new Branch(unboxBlock); + block.Instructions[^1] = falseInst; unboxBlock.Instructions.RemoveAt(0); if (unboxOperand == tempStore?.Variable) { @@ -291,6 +634,7 @@ private bool PatternMatchValueTypes(Block block, BlockContainer container, ILTra // the pattern matching machinery to an offset belonging to an instruction in the then-block. unboxBlock.SetILRange(unboxBlock.Instructions[0]); storeToV.Variable.Kind = VariableKind.PatternLocal; + DetectPropertySubPatterns((MatchInstruction)ifInst.Condition, unboxBlock, falseInst, container, context, ref cfg); return true; } @@ -307,15 +651,14 @@ private bool MatchIsInstBlock(Block block, [NotNullWhen(true)] out ILVariable? testedVariable, out IType? boxType, [NotNullWhen(true)] out Block? unboxBlock, - [NotNullWhen(true)] out Block? falseBlock) + [NotNullWhen(true)] out ILInstruction? falseInst) { type = null; testedOperand = null; testedVariable = null; boxType = null; unboxBlock = null; - falseBlock = null; - if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) + if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out falseInst)) { return false; } @@ -343,8 +686,7 @@ private bool MatchIsInstBlock(Block block, { return false; } - return trueInst.MatchBranch(out unboxBlock) && falseInst.MatchBranch(out falseBlock) - && unboxBlock.Parent == block.Parent && falseBlock.Parent == block.Parent; + return trueInst.MatchBranch(out unboxBlock) && unboxBlock.Parent == block.Parent; } /// Block unboxBlock (incoming: 1) { diff --git a/ICSharpCode.Decompiler/Util/Index.cs b/ICSharpCode.Decompiler/Util/Index.cs new file mode 100644 index 0000000000..6da2439e74 --- /dev/null +++ b/ICSharpCode.Decompiler/Util/Index.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable enable +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value { + get { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); +#else + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); +#endif + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} \ No newline at end of file diff --git a/ILSpy.Installer/ILSpy.Installer.csproj b/ILSpy.Installer/ILSpy.Installer.csproj index 6567b67ae2..0697d38728 100644 --- a/ILSpy.Installer/ILSpy.Installer.csproj +++ b/ILSpy.Installer/ILSpy.Installer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,6 +6,10 @@ ILSpy.Installer.Builder + + $(DefineConstants);$(PlatformForInstaller) + + diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 54b1ce17b6..18a93692ff 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1145,6 +1145,15 @@ public static string DecompilerSettings_ParameterNullCheck { } } + /// + /// Looks up a localized string similar to Pattern combinators (and, or, not). + /// + public static string DecompilerSettings_PatternCombinators { + get { + return ResourceManager.GetString("DecompilerSettings.PatternCombinators", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use pattern matching expressions. /// @@ -1199,6 +1208,24 @@ public static string DecompilerSettings_RecordStructs { } } + /// + /// Looks up a localized string similar to Recursive pattern matching. + /// + public static string DecompilerSettings_RecursivePatternMatching { + get { + return ResourceManager.GetString("DecompilerSettings.RecursivePatternMatching", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Relational patterns. + /// + public static string DecompilerSettings_RelationalPatterns { + get { + return ResourceManager.GetString("DecompilerSettings.RelationalPatterns", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove dead and side effect free code (use with caution!). /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 5187179228..f63a10682d 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -405,6 +405,9 @@ Are you sure you want to continue? Use parameter null checking + + Pattern combinators (and, or, not) + Use pattern matching expressions @@ -423,6 +426,12 @@ Are you sure you want to continue? Record structs + + Recursive pattern matching + + + Relational patterns + Remove dead and side effect free code (use with caution!)