diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs index 5b9f38787..cbc56794b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PatternMatching.cs @@ -4,6 +4,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { public class PatternMatching { + public class X + { + public int A { get; set; } + public string B { get; set; } + public object C { get; set; } + } + public void SimpleTypePattern(object x) { if (x is string value) @@ -303,6 +310,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + public void RecursivePattern_Type(object x) + { + if (x is X { C: string text }) + { + Console.WriteLine("Test " + text); + } + else + { + Console.WriteLine("not Test"); + } + } + private bool F() { return true; diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 707377b47..c73084370 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4564,9 +4564,45 @@ namespace ICSharpCode.Decompiler.CSharp throw new NotImplementedException(); if (matchInstruction.IsDeconstructTuple) throw new NotImplementedException(); - if (matchInstruction.SubPatterns.Any()) - throw new NotImplementedException(); - if (matchInstruction.HasDesignator) + if (matchInstruction.SubPatterns.Count > 0) + { + RecursivePatternExpression recursivePatternExpression = new(); + recursivePatternExpression.Type = ConvertType(matchInstruction.Variable.Type); + foreach (var subPattern in matchInstruction.SubPatterns) + { + if (!MatchInstruction.IsPatternMatch(subPattern, out var testedOperand)) + { + 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) } + .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) { SingleVariableDesignation designator = new SingleVariableDesignation { Identifier = matchInstruction.Variable.Name }; designator.AddAnnotation(new ILVariableResolveResult(matchInstruction.Variable)); diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 3e428e5ef..152f9fdd8 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -948,6 +948,40 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor 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); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index 41870ba48..2016b429c 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -512,6 +512,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax VisitChildren(declarationExpression); } + public virtual void VisitRecursivePatternExpression(RecursivePatternExpression recursivePatternExpression) + { + VisitChildren(recursivePatternExpression); + } + public virtual void VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { VisitChildren(outVarDeclarationExpression); @@ -1190,6 +1195,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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 0e8585233..558456524 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs @@ -52,7 +52,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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 000000000..3a26d735b --- /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/IAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs index 25fa54d02..37dfd40a3 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs @@ -36,6 +36,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 5ee433d8a..3f8c72492 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/Transforms/PatternMatchingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs index 9a02d9c7e..7cdc090db 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs @@ -22,6 +22,7 @@ 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 +40,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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)) { @@ -179,9 +180,50 @@ namespace ICSharpCode.Decompiler.IL.Transforms block.Instructions[block.Instructions.Count - 1] = falseInst; block.Instructions.RemoveRange(pos, ifInst.ChildIndex - pos); v.Kind = VariableKind.PatternLocal; + + if (trueInst.MatchBranch(out var trueBlock) && trueBlock.IncomingEdgeCount == 1 && trueBlock.Parent == container) + { + DetectPropertySubPatterns((MatchInstruction)ifInst.Condition, trueBlock, falseInst); + } + return true; } + private bool DetectPropertySubPatterns(MatchInstruction parentPattern, Block block, ILInstruction parentFalseInst) + { + // if (match.notnull.type[System.String] (V_0 = callvirt get_C(ldloc V_2))) br IL_0022 + // br IL_0037 + if (block.Instructions.Count == 2 && block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) + { + if (MatchInstruction.IsPatternMatch(condition, out var operand)) + { + if (operand is not CallInstruction { + Method: { + SymbolKind: SymbolKind.Accessor, + AccessorKind: MethodSemanticsAttributes.Getter + }, + Arguments: [LdLoc ldloc] + } call) + { + return false; + } + if (ldloc.Variable != parentPattern.Variable) + { + return false; + } + if (!DetectExitPoints.CompatibleExitInstruction(parentFalseInst, falseInst)) + { + return false; + } + parentPattern.SubPatterns.Add(condition); + block.Instructions.RemoveAt(0); + block.Instructions[0] = trueInst; + return true; + } + } + return false; + } + private bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILInstruction trueInst, ILInstruction storeToV, ILInstruction? loadInNullCheck, ILTransformContext context, ref ControlFlowGraph? cfg) { diff --git a/ICSharpCode.Decompiler/Util/Index.cs b/ICSharpCode.Decompiler/Util/Index.cs new file mode 100644 index 000000000..6da2439e7 --- /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