diff --git a/BuildTools/update-assemblyinfo.ps1 b/BuildTools/update-assemblyinfo.ps1 index f021a331d..dbfa5f911 100644 --- a/BuildTools/update-assemblyinfo.ps1 +++ b/BuildTools/update-assemblyinfo.ps1 @@ -53,7 +53,7 @@ function gitCommitHash() { if (No-Git) { return "0000000000000000000000000000000000000000"; } - return (git rev-list "$baseCommit..HEAD") | Select -First 1; + return (git rev-list --max-count 1 HEAD); } function gitBranch() { @@ -150,7 +150,7 @@ try { $out = $out.Replace('$INSERTVERSIONNAMEPOSTFIX$', $postfixVersionName); $out = $out.Replace('$INSERTBUILDCONFIG$', $buildConfig); - if (((Get-Content $file.Input) -Join [System.Environment]::NewLine) -ne $out) { + if ((-not (Test-File $file.Output)) -or (((Get-Content $file.Output) -Join [System.Environment]::NewLine) -ne $out)) { $out | Out-File -Encoding utf8 $file.Output; } } @@ -174,7 +174,7 @@ try { $out = $out.Replace('$INSERTVERSIONNAMEPOSTFIX$', $postfixVersionName); $out = $out.Replace('$INSERTBUILDCONFIG$', $buildConfig); - if (((Get-Content $file.Input) -Join [System.Environment]::NewLine) -ne $out) { + if ((-not (Test-File $file.Output)) -or (((Get-Content $file.Output) -Join [System.Environment]::NewLine) -ne $out)) { $out | Out-File -Encoding utf8 $file.Output; } } diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index ebd186188..ac9c66bf6 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -202,6 +202,12 @@ namespace ICSharpCode.Decompiler.Tests RunCS(options: options); } + [Test] + public void DeconstructionTests([ValueSource("roslynOnlyOptions")] CompilerOptions options) + { + RunCS(options: options); + } + [Test] public void BitNot([Values(false, true)] bool force32Bit) { diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 62b9b5535..93cbbe251 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -91,7 +91,9 @@ + + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 48bbeba88..5754913a9 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -500,6 +500,12 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(cscOptions: cscOptions); } + [Test] + public void DeconstructionTests([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions); + } + void RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, DecompilerSettings decompilerSettings = null) { Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, decompilerSettings); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs new file mode 100644 index 000000000..f27372aca --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness +{ + class DeconstructionTests + { + public static void Main() + { + new DeconstructionTests().Test(); + } + + public struct MyInt + { + public static implicit operator int(MyInt x) + { + Console.WriteLine("int op_Implicit(MyInt)"); + return 0; + } + + public static implicit operator MyInt(int x) + { + Console.WriteLine("MyInt op_Implicit(int)"); + return default(MyInt); + } + } + + private class DeconstructionSource + { + public int Dummy { + get; + set; + } + + public void Deconstruct(out T a, out T2 b) + { + Console.WriteLine("Deconstruct"); + a = default(T); + b = default(T2); + } + } + + private class AssignmentTargets + { + int id; + + public AssignmentTargets(int id) + { + this.id = id; + } + + public int IntField; + + public int? NullableIntField; + + public MyInt MyIntField; + + public MyInt? NullableMyIntField; + + public MyInt My { + get { + Console.WriteLine($"{id}.get_My()"); + return default; + } + set { + Console.WriteLine($"{id}.set_My({value})"); + } + } + + public MyInt? NMy { + get { + Console.WriteLine($"{id}.get_NMy()"); + return default; + } + set { + Console.WriteLine($"{id}.set_NMy({value})"); + } + } + + public int IntProperty { + get { + Console.WriteLine($"{id}.get_IntProperty()"); + return default; + } + set { + Console.WriteLine($"{id}.set_IntProperty({value})"); + } + } + + public uint UIntProperty { + get { + Console.WriteLine($"{id}.get_UIntProperty()"); + return default; + } + set { + Console.WriteLine($"{id}.set_UIntProperty({value})"); + } + } + } + + private DeconstructionSource GetSource() + { + Console.WriteLine("GetSource()"); + return new DeconstructionSource(); + } + + private (T, T2) GetTuple() + { + Console.WriteLine("GetTuple()"); + return default(ValueTuple); + } + + private (T, T2, T3) GetTuple() + { + Console.WriteLine("GetTuple()"); + return default(ValueTuple); + } + + private AssignmentTargets Get(int i) + { + Console.WriteLine($"Get({i})"); + return new AssignmentTargets(i); + } + + public void Test() + { + Property_NoDeconstruction_SwappedAssignments(); + Property_NoDeconstruction_SwappedInits(); + Property_IntToUIntConversion(); + NoDeconstruction_NotUsingConver(); + NoDeconstruction_NotUsingConver_Tuple(); + NullReferenceException_Field_Deconstruction(out _); + NullReferenceException_RefLocalReferencesField_Deconstruction(out _); + NullReferenceException_RefLocalReferencesArrayElement_Deconstruction(out _, null); + DeconstructTupleSameVar(("a", "b")); + DeconstructTupleListForEachSameVar(new List<(string, string)> { ("a", "b") }); + } + + public void Property_NoDeconstruction_SwappedAssignments() + { + Console.WriteLine("Property_NoDeconstruction_SwappedAssignments:"); + AssignmentTargets customDeconstructionAndConversion = Get(0); + AssignmentTargets customDeconstructionAndConversion2 = Get(1); + GetSource().Deconstruct(out MyInt? x, out MyInt y); + MyInt myInt2 = customDeconstructionAndConversion2.My = y; + MyInt? myInt4 = customDeconstructionAndConversion.NMy = x; + } + + public void Property_NoDeconstruction_SwappedInits() + { + Console.WriteLine("Property_NoDeconstruction_SwappedInits:"); + AssignmentTargets customDeconstructionAndConversion = Get(1); + (Get(0).NMy, customDeconstructionAndConversion.My) = GetSource(); + } + + public void Property_IntToUIntConversion() + { + Console.WriteLine("Property_IntToUIntConversion:"); + AssignmentTargets t0 = Get(0); + AssignmentTargets t1 = Get(1); + int a; + uint b; + GetSource().Deconstruct(out a, out b); + t0.UIntProperty = (uint)a; + t1.IntProperty = (int)b; + } + + public void NoDeconstruction_NotUsingConver() + { + Console.WriteLine("NoDeconstruction_NotUsingConver:"); + AssignmentTargets t0 = Get(0); + int a; + uint b; + GetSource().Deconstruct(out a, out b); + long c = a; + t0.IntProperty = a; + t0.UIntProperty = b; + Console.WriteLine(c); + } + + public void NoDeconstruction_NotUsingConver_Tuple() + { + Console.WriteLine("NoDeconstruction_NotUsingConver_Tuple:"); + AssignmentTargets t0 = Get(0); + var t = GetTuple(); + long c = t.Item1; + t0.IntProperty = t.Item1; + t0.UIntProperty = t.Item2; + Console.WriteLine(c); + } + + public void NullReferenceException_Field_Deconstruction(out int a) + { + try { + AssignmentTargets t0 = null; + (t0.IntField, a) = GetSource(); + } catch (Exception ex) { + a = 0; + Console.WriteLine(ex.GetType().FullName); + } + } + + public void NullReferenceException_RefLocalReferencesField_Deconstruction(out int a) + { + try { + AssignmentTargets t0 = null; + ref int i = ref t0.IntField; + (i, a) = GetSource(); + } catch (Exception ex) { + a = 0; + Console.WriteLine(ex.GetType().FullName); + } + } + + public void NullReferenceException_RefLocalReferencesArrayElement_Deconstruction(out int a, int[] arr) + { + try { + ref int i = ref arr[0]; + (i, a) = GetSource(); + } catch (Exception ex) { + a = 0; + Console.WriteLine(ex.GetType().FullName); + } + } + + public void DeconstructTupleSameVar((string, string) tuple) + { + Console.WriteLine("DeconstructTupleSameVar:"); + string a; + a = tuple.Item1; + a = tuple.Item2; + Console.WriteLine(a); + } + + public void DeconstructTupleListForEachSameVar(List<(string, string)> tuples) + { + Console.WriteLine("DeconstructTupleListForEachSameVar:"); + foreach (var tuple in tuples) { + string a; + a = tuple.Item1; + a = tuple.Item2; + Console.WriteLine(a); + } + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs new file mode 100644 index 000000000..566a56c3e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs @@ -0,0 +1,596 @@ +// Copyright (c) 2020 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. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public static class DeconstructionExt + { + public static void Deconstruct(this KeyValuePair pair, out K key, out V value) + { + key = pair.Key; + value = pair.Value; + } + } + + internal class DeconstructionTests + { + [StructLayout(LayoutKind.Sequential, Size = 1)] + public struct MyInt + { + public static implicit operator int(MyInt x) + { + return 0; + } + + public static implicit operator MyInt(int x) + { + return default(MyInt); + } + } + + private class DeconstructionSource + { + public int Dummy { + get; + set; + } + + public void Deconstruct(out T a, out T2 b) + { + a = default(T); + b = default(T2); + } + } + + private class DeconstructionSource + { + public int Dummy { + get; + set; + } + + public void Deconstruct(out T a, out T2 b, out T3 c) + { + a = default(T); + b = default(T2); + c = default(T3); + } + } + + private class AssignmentTargets + { + public int IntField; + public long LongField; + public float FloatField; + public double DoubleField; + public decimal DecimalField; + + public MyInt MyField; + public MyInt? NMyField; + + public string StringField; + public object ObjectField; + public dynamic DynamicField; + + public int? NullableIntField; + + public MyInt MyIntField; + + public MyInt? NullableMyIntField; + + public int Int { + get; + set; + } + + public long Long { + get; + set; + } + + public float Float { + get; + set; + } + + public double Double { + get; + set; + } + + public decimal Decimal { + get; + set; + } + + public string String { + get; + set; + } + + public object Object { + get; + set; + } + + public dynamic Dynamic { + get; + set; + } + + public int? NInt { + get; + set; + } + + public MyInt My { + get; + set; + } + + public MyInt? NMy { + get; + set; + } + + public static MyInt StaticMy { + get; + set; + } + + public static MyInt? StaticNMy { + get; + set; + } + } + + private DeconstructionSource GetSource() + { + return null; + } + + private DeconstructionSource GetSource() + { + return null; + } + + private ref T GetRef() + { + throw new NotImplementedException(); + } + + private (T, T2) GetTuple() + { + return default((T, T2)); + } + + private (T, T2, T3) GetTuple() + { + return default((T, T2, T3)); + } + + private AssignmentTargets Get(int i) + { + return null; + } + + public void LocalVariable_NoConversion_Custom() + { + var (myInt3, x) = GetSource(); + Console.WriteLine(myInt3); + Console.WriteLine(x); + } + + public void LocalVariable_NoConversion_Tuple() + { + var (myInt, x) = GetTuple(); + Console.WriteLine(myInt); + Console.WriteLine(x); + } + + public void LocalVariable_NoConversion_Custom_DiscardFirst() + { + var (_, x, value) = GetSource(); + Console.WriteLine(x); + Console.WriteLine(value); + } + + // currently we detect deconstruction, iff the first element is not discarded + //public void LocalVariable_NoConversion_Tuple_DiscardFirst() + //{ + // var (_, x, value) = GetTuple(); + // Console.WriteLine(x); + // Console.WriteLine(value); + //} + + public void LocalVariable_NoConversion_Custom_DiscardLast() + { + var (myInt3, x, _) = GetSource(); + Console.WriteLine(myInt3); + Console.WriteLine(x); + } + + public void LocalVariable_NoConversion_Tuple_DiscardLast() + { + var (myInt, x, _) = GetTuple(); + Console.WriteLine(myInt); + Console.WriteLine(x); + } + + public void LocalVariable_NoConversion_Custom_DiscardSecond() + { + var (myInt3, _, value) = GetSource(); + Console.WriteLine(myInt3); + Console.WriteLine(value); + } + + public void LocalVariable_NoConversion_Tuple_DiscardSecond() + { + var (myInt, _, value) = GetTuple(); + Console.WriteLine(myInt); + Console.WriteLine(value); + } + + public void LocalVariable_NoConversion_Custom_ReferenceTypes() + { + var (value, value2) = GetSource(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_NoConversion_Tuple_ReferenceTypes() + { + var (value, value2) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_IntToLongConversion_Custom() + { + int value; + long value2; + (value, value2) = GetSource(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_IntToLongConversion_Tuple() + { + int value; + long value2; + (value, value2) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_FloatToDoubleConversion_Custom() + { + int value; + double value2; + (value, value2) = GetSource(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_FloatToDoubleConversion_Tuple() + { + int value; + double value2; + (value, value2) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + // dynamic conversion is currently not supported + //public void LocalVariable_ImplicitReferenceConversion_Custom() + //{ + // object value; + // dynamic value2; + // (value, value2) = GetSource(); + // Console.WriteLine(value); + // value2.UseMe(); + //} + + //public void LocalVariable_ImplicitReferenceConversion_Tuple() + //{ + // object value; + // dynamic value2; + // (value, value2) = GetTuple(); + // Console.WriteLine(value); + // value2.UseMe(); + //} + + public void LocalVariable_NoConversion_ComplexValue_Custom() + { + var (myInt3, x) = new DeconstructionSource { + Dummy = 3 + }; + Console.WriteLine(myInt3); + Console.WriteLine(x); + } + + public void Property_NoConversion_Custom() + { + (Get(0).NMy, Get(1).My) = GetSource(); + } + + public void Property_IntToLongConversion_Custom() + { + (Get(0).Int, Get(1).Long) = GetSource(); + } + + public void Property_FloatToDoubleConversion_Custom() + { + (Get(0).Int, Get(1).Double) = GetSource(); + } + + // dynamic conversion is not supported + //public void Property_ImplicitReferenceConversion_Custom() + //{ + // (Get(0).Object, Get(1).Dynamic) = GetSource(); + //} + + public void Property_NoConversion_Custom_DiscardFirst() + { + (_, Get(1).My) = GetSource(); + } + + public void Property_NoConversion_Custom_DiscardLast() + { + (Get(0).NMy, _) = GetSource(); + } + + public void Property_NoConversion_Tuple() + { + (Get(0).NMy, Get(1).My) = GetTuple(); + } + + public void Property_NoConversion_Tuple_DiscardLast() + { + (Get(0).NMy, Get(1).My, _) = GetTuple(); + } + + // currently we detect deconstruction, iff the first element is not discarded + //public void Property_NoConversion_Tuple_DiscardFirst() + //{ + // (_, Get(1).My, Get(2).Int) = GetTuple(); + //} + + public void Property_NoConversion_Custom_DiscardSecond() + { + (Get(0).NMy, _, Get(2).Int) = GetSource(); + } + + public void Property_NoConversion_Tuple_DiscardSecond() + { + (Get(0).NMy, _, Get(2).Int) = GetTuple(); + } + + public void Property_NoConversion_Custom_ReferenceTypes() + { + (Get(0).String, Get(1).String) = GetSource(); + } + + public void Property_NoConversion_Tuple_ReferenceTypes() + { + (Get(0).String, Get(1).String) = GetTuple(); + } + + public void Property_IntToLongConversion_Tuple() + { + (Get(0).Int, Get(1).Long) = GetTuple(); + } + + public void Property_FloatToDoubleConversion_Tuple() + { + (Get(0).Int, Get(1).Double) = GetTuple(); + } + + public void RefLocal_NoConversion_Custom(out double a) + { + (a, GetRef()) = GetSource(); + } + + public void RefLocal_NoConversion_Tuple(out double a) + { + (a, GetRef()) = GetTuple(); + } + + public void RefLocal_FloatToDoubleConversion_Custom(out double a) + { + (a, GetRef()) = GetSource(); + } + + public void RefLocal_FloatToDoubleConversion_Custom2(out double a) + { + (a, GetRef()) = GetSource(); + } + + public void RefLocal_FloatToDoubleConversion_Tuple(out double a) + { + (a, GetRef()) = GetTuple(); + } + + public void RefLocal_NoConversion_Custom(out MyInt? a) + { + (a, GetRef()) = GetSource(); + } + + public void RefLocal_IntToLongConversion_Custom(out long a) + { + (a, GetRef()) = GetSource(); + } + + // dynamic conversion is not supported + //public void RefLocal_ImplicitReferenceConversion_Custom(out object a) + //{ + // (a, GetRef()) = GetSource(); + //} + + public void RefLocal_NoConversion_Custom_DiscardFirst() + { + (_, GetRef()) = GetSource(); + } + + public void RefLocal_NoConversion_Custom_DiscardLast(out MyInt? a) + { + (a, _) = GetSource(); + } + + public void RefLocal_NoConversion_Tuple(out MyInt? a) + { + (a, GetRef()) = GetTuple(); + } + + public void RefLocal_NoConversion_Tuple_DiscardLast(out MyInt? a) + { + (a, GetRef(), _) = GetTuple(); + } + + // currently we detect deconstruction, iff the first element is not discarded + //public void RefLocal_NoConversion_Tuple_DiscardFirst(out var a) + //{ + // (_, GetRef(), GetRef()) = GetTuple(); + //} + + public void RefLocal_NoConversion_Custom_DiscardSecond(out MyInt? a) + { + (a, _, GetRef()) = GetSource(); + } + + public void RefLocal_NoConversion_Tuple_DiscardSecond(out MyInt? a) + { + (a, _, GetRef()) = GetTuple(); + } + + public void RefLocal_NoConversion_Custom_ReferenceTypes(out string a) + { + (a, GetRef()) = GetSource(); + } + + public void RefLocal_NoConversion_Tuple_ReferenceTypes(out string a) + { + (a, GetRef()) = GetTuple(); + } + + public void RefLocal_IntToLongConversion_Tuple(out long a) + { + (a, GetRef()) = GetTuple(); + } + + //public void ArrayAssign_FloatToDoubleConversion_Custom(double[] arr) + //{ + // (arr[0], arr[1], arr[2]) = GetSource(); + //} + + public void Field_NoConversion_Custom() + { + (Get(0).IntField, Get(1).IntField) = GetSource(); + } + + public void Field_NoConversion_Tuple() + { + (Get(0).IntField, Get(1).IntField) = GetTuple(); + } + + public void Field_IntToLongConversion_Custom() + { + (Get(0).IntField, Get(1).LongField) = GetSource(); + } + + public void Field_IntToLongConversion_Tuple() + { + (Get(0).IntField, Get(1).LongField) = GetTuple(); + } + + public void Field_FloatToDoubleConversion_Custom() + { + (Get(0).DoubleField, Get(1).DoubleField) = GetSource(); + } + + public void Field_FloatToDoubleConversion_Tuple() + { + (Get(0).DoubleField, Get(1).DoubleField) = GetTuple(); + } + + // dynamic conversion is not supported + //public void Field_ImplicitReferenceConversion_Custom() + //{ + // (Get(0).ObjectField, Get(1).DynamicField) = GetSource(); + //} + + public void Field_NoConversion_Custom_DiscardFirst() + { + (_, Get(1).MyField) = GetSource(); + } + + public void Field_NoConversion_Custom_DiscardLast() + { + (Get(0).NMyField, _) = GetSource(); + } + + public void Field_NoConversion_Tuple_DiscardLast() + { + (Get(0).NMyField, Get(1).MyField, _) = GetTuple(); + } + + // currently we detect deconstruction, iff the first element is not discarded + //public void Field_NoConversion_Tuple_DiscardFirst() + //{ + // (_, Get(1).MyField, Get(2).IntField) = GetTuple(); + //} + + public void Field_NoConversion_Custom_DiscardSecond() + { + (Get(0).NMyField, _, Get(2).IntField) = GetSource(); + } + + public void Field_NoConversion_Tuple_DiscardSecond() + { + (Get(0).NMyField, _, Get(2).IntField) = GetTuple(); + } + + public void Field_NoConversion_Custom_ReferenceTypes() + { + (Get(0).StringField, Get(1).StringField) = GetSource(); + } + + public void Field_NoConversion_Tuple_ReferenceTypes() + { + (Get(0).StringField, Get(1).StringField) = GetTuple(); + } + + public void DeconstructDictionaryForEach(Dictionary dictionary) + { + foreach (var (str, num2) in dictionary) { + Console.WriteLine(str + ": " + num2); + } + } + + public void DeconstructTupleListForEach(List<(string, int)> tuples) + { + foreach (var (str, num) in tuples) { + Console.WriteLine(str + ": " + num); + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 40517bfc8..c09132e42 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -144,6 +144,7 @@ namespace ICSharpCode.Decompiler.CSharp new TransformCollectionAndObjectInitializers(), new TransformExpressionTrees(), new IndexRangeTransform(), + new DeconstructionTransform(), new NamedArgumentTransform(), new UserDefinedLogicTransform() ), diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index edbdf911d..189c94744 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -3386,6 +3386,88 @@ namespace ICSharpCode.Decompiler.CSharp return invocation.WithRR(new ResolveResult(inst.ReturnType)).WithILInstruction(inst); } + protected internal override TranslatedExpression VisitDeconstructInstruction(DeconstructInstruction inst, TranslationContext context) + { + IType rhsType = inst.Pattern.Variable.Type; + var rhs = Translate(inst.Pattern.TestedOperand, rhsType); + rhs = rhs.ConvertTo(rhsType, this); // TODO allowImplicitConversion + var assignments = inst.Assignments.Instructions; + int assignmentPos = 0; + var inits = inst.Init; + int initPos = 0; + + Dictionary conversionMapping = new Dictionary(); + + foreach (var conv in inst.Conversions.Instructions) { + if (!DeconstructInstruction.IsConversionStLoc(conv, out var outputVariable, out var inputVariable)) + continue; + conversionMapping.Add(inputVariable, outputVariable); + } + + + var lhs = ConstructTuple(inst.Pattern); + return new AssignmentExpression(lhs, rhs) + .WithILInstruction(inst) + .WithRR(new ResolveResult(compilation.FindType(KnownTypeCode.Void))); + + TupleExpression ConstructTuple(MatchInstruction matchInstruction) + { + var expr = new TupleExpression(); + foreach (var subPattern in matchInstruction.SubPatterns.Cast()) { + if (subPattern.IsVar) { + if (subPattern.HasDesignator) { + if (!conversionMapping.TryGetValue(subPattern.Variable, out ILVariable value)) { + value = subPattern.Variable; + } + expr.Elements.Add(ConstructAssignmentTarget(assignments[assignmentPos], value)); + assignmentPos++; + } else + expr.Elements.Add(new IdentifierExpression("_")); + } else { + expr.Elements.Add(ConstructTuple(subPattern)); + } + } + return expr; + } + + TranslatedExpression ConstructAssignmentTarget(ILInstruction assignment, ILVariable value) + { + switch (assignment) { + case StLoc stloc: + Debug.Assert(stloc.Value.MatchLdLoc(value)); + break; + case CallInstruction call: + for (int i = 0; i < call.Arguments.Count - 1; i++) { + ReplaceAssignmentTarget(call.Arguments[i]); + } + Debug.Assert(call.Arguments.Last().MatchLdLoc(value)); + break; + case StObj stobj: + var target = stobj.Target; + while (target.MatchLdFlda(out var nestedTarget, out _)) + target = nestedTarget; + ReplaceAssignmentTarget(target); + Debug.Assert(stobj.Value.MatchLdLoc(value)); + break; + default: + throw new NotSupportedException(); + } + var expr = Translate(assignment); + return expr.UnwrapChild(((AssignmentExpression)expr).Left); + } + + void ReplaceAssignmentTarget(ILInstruction target) + { + if (target.MatchLdLoc(out var v) + && v.Kind == VariableKind.DeconstructionInitTemporary) + { + Debug.Assert(inits[initPos].Variable == v); + target.ReplaceWith(inits[initPos].Value); + initPos++; + } + } + } + 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 30caec5dc..50295d933 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -791,6 +791,17 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor EndNode(directionExpression); } + public virtual void VisitDeclarationExpression(DeclarationExpression declarationExpression) + { + StartNode(declarationExpression); + + declarationExpression.Type.AcceptVisitor(this); + Space(); + declarationExpression.Designation.AcceptVisitor(this); + + EndNode(declarationExpression); + } + public virtual void VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { StartNode(outVarDeclarationExpression); @@ -1551,7 +1562,8 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor Space(policy.SpacesWithinForeachParentheses); foreachStatement.VariableType.AcceptVisitor(this); Space(); - WriteIdentifier(foreachStatement.VariableNameToken); + foreachStatement.VariableDesignation.AcceptVisitor(this); + Space(); WriteKeyword(ForeachStatement.InKeywordRole); Space(); foreachStatement.InExpression.AcceptVisitor(this); @@ -2410,6 +2422,22 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor EndNode(primitiveType); } + public virtual void VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation) + { + StartNode(singleVariableDesignation); + writer.WriteIdentifier(singleVariableDesignation.IdentifierToken); + EndNode(singleVariableDesignation); + } + + public virtual void VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation) + { + StartNode(parenthesizedVariableDesignation); + LPar(); + WriteCommaSeparatedList(parenthesizedVariableDesignation.VariableDesignations); + RPar(); + EndNode(parenthesizedVariableDesignation); + } + public virtual void VisitComment(Comment comment) { writer.StartNode(comment); diff --git a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs index 7993932c9..072424663 100644 --- a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs @@ -269,7 +269,7 @@ namespace ICSharpCode.Decompiler.CSharp StartSequencePoint(foreachStatement); AddToSequencePoint(foreachInfo.GetCurrentCall); - EndSequencePoint(foreachStatement.VariableType.StartLocation, foreachStatement.VariableNameToken.EndLocation); + EndSequencePoint(foreachStatement.VariableType.StartLocation, foreachStatement.VariableDesignation.EndLocation); VisitAsSequencePoint(foreachStatement.EmbeddedStatement); } diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index b1d6dd1eb..d9a461566 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -504,12 +504,9 @@ namespace ICSharpCode.Decompiler.CSharp if (enumeratorVar2 != enumeratorVar) return null; // Detect which foreach-variable transformation is necessary/possible. - var transformation = DetectGetCurrentTransformation(container, body, enumeratorVar, conditionInst, out var singleGetter, out var foreachVariable); + var transformation = DetectGetCurrentTransformation(container, body, loopContainer, enumeratorVar, conditionInst, out var singleGetter, out var foreachVariable); if (transformation == RequiredGetCurrentTransformation.NoForeach) return null; - // The existing foreach variable, if found, can only be used in the loop container. - if (foreachVariable != null && !(foreachVariable.CaptureScope == null || foreachVariable.CaptureScope == loopContainer)) - return null; // Extract in-expression var collectionExpr = m.Get("collection").Single(); // Special case: foreach (var item in this) is decompiled as foreach (var item in base) @@ -546,6 +543,8 @@ namespace ICSharpCode.Decompiler.CSharp break; } + VariableDesignation designation = null; + // Handle the required foreach-variable transformation: switch (transformation) { case RequiredGetCurrentTransformation.UseExistingVariable: @@ -575,7 +574,18 @@ namespace ICSharpCode.Decompiler.CSharp body.Instructions.Insert(0, new StLoc(localCopyVariable, new LdLoc(foreachVariable))); body.Instructions.Insert(0, new StLoc(foreachVariable, instToReplace)); break; + case RequiredGetCurrentTransformation.Deconstruction: + useVar = true; + designation = TranslateDeconstructionDesignation((DeconstructInstruction)body.Instructions[0], isForeach: true); + break; + } + + if (designation == null) { + designation = new SingleVariableDesignation { Identifier = foreachVariable.Name }; + // Add the variable annotation for highlighting + designation.AddAnnotation(new ILVariableResolveResult(foreachVariable, foreachVariable.Type)); } + // Convert the modified body to C# AST: var whileLoop = (WhileStatement)ConvertAsBlock(container).First(); BlockStatement foreachBody = (BlockStatement)whileLoop.EmbeddedStatement.Detach(); @@ -595,12 +605,10 @@ namespace ICSharpCode.Decompiler.CSharp // Construct the foreach loop. var foreachStmt = new ForeachStatement { VariableType = useVar ? new SimpleType("var") : exprBuilder.ConvertType(foreachVariable.Type), - VariableName = foreachVariable.Name, + VariableDesignation = designation, InExpression = collectionExpr.Detach(), EmbeddedStatement = foreachBody }; - // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(foreachVariable, foreachVariable.Type)); foreachStmt.AddAnnotation(new ForeachAnnotation(inst.ResourceExpression, conditionInst, singleGetter)); foreachStmt.CopyAnnotationsFrom(whileLoop); // If there was an optional return statement, return it as well. @@ -615,6 +623,38 @@ namespace ICSharpCode.Decompiler.CSharp return foreachStmt; } + internal static VariableDesignation TranslateDeconstructionDesignation(DeconstructInstruction inst, bool isForeach) + { + var assignments = inst.Assignments.Instructions; + int assignmentPos = 0; + + return ConstructDesignation(inst.Pattern); + + VariableDesignation ConstructDesignation(MatchInstruction matchInstruction) + { + var designations = new ParenthesizedVariableDesignation(); + foreach (var subPattern in matchInstruction.SubPatterns.Cast()) { + if (subPattern.IsVar) { + var designation = new SingleVariableDesignation(); + if (subPattern.HasDesignator) { + ILVariable v = ((StLoc)assignments[assignmentPos]).Variable; + if (isForeach) + v.Kind = VariableKind.ForeachLocal; + designation.Identifier = v.Name; + designation.AddAnnotation(new ILVariableResolveResult(v)); + assignmentPos++; + } else { + designation.Identifier = "_"; + } + designations.VariableDesignations.Add(designation); + } else { + designations.VariableDesignations.Add(ConstructDesignation(subPattern)); + } + } + return designations; + } + } + static bool EqualErasedType(IType a, IType b) { return NormalizeTypeVisitor.TypeErasure.EquivalentTypes(a, b); @@ -703,7 +743,12 @@ namespace ICSharpCode.Decompiler.CSharp /// ... (ldloca copy) ... /// /// - IntroduceNewVariableAndLocalCopy + IntroduceNewVariableAndLocalCopy, + /// + /// call get_Current() is the tested operand of a deconstruct instruction. + /// and the deconstruct instruction is the first statement in the loop body. + /// + Deconstruction, } /// @@ -716,11 +761,14 @@ namespace ICSharpCode.Decompiler.CSharp /// Returns the call instruction invoking Current's getter. /// Returns the the foreach variable, if a suitable was found. This variable is only assigned once and its assignment is the first statement in . /// for details. - RequiredGetCurrentTransformation DetectGetCurrentTransformation(BlockContainer usingContainer, Block loopBody, ILVariable enumerator, ILInstruction moveNextUsage, out CallInstruction singleGetter, out ILVariable foreachVariable) + RequiredGetCurrentTransformation DetectGetCurrentTransformation(BlockContainer usingContainer, Block loopBody, BlockContainer loopContainer, ILVariable enumerator, ILInstruction moveNextUsage, out CallInstruction singleGetter, out ILVariable foreachVariable) { singleGetter = null; foreachVariable = null; - var loads = (enumerator.LoadInstructions.OfType().Concat(enumerator.AddressInstructions.OfType())).Where(ld => !ld.IsDescendantOf(moveNextUsage)).ToArray(); + var loads = enumerator.LoadInstructions.OfType() + .Concat(enumerator.AddressInstructions.OfType()) + .Where(ld => !ld.IsDescendantOf(moveNextUsage)) + .ToArray(); // enumerator is used in multiple locations or not in conjunction with get_Current // => no foreach if (loads.Length != 1 || !ParentIsCurrentGetter(loads[0])) @@ -728,8 +776,17 @@ namespace ICSharpCode.Decompiler.CSharp singleGetter = (CallInstruction)loads[0].Parent; // singleGetter is not part of the first instruction in body or cannot be uninlined // => no foreach - if (!(singleGetter.IsDescendantOf(loopBody.Instructions[0]) && ILInlining.CanUninline(singleGetter, loopBody.Instructions[0]))) + if (!(singleGetter.IsDescendantOf(loopBody.Instructions[0]) + && ILInlining.CanUninline(singleGetter, loopBody.Instructions[0]))) + { return RequiredGetCurrentTransformation.NoForeach; + } + if (loopBody.Instructions[0] is DeconstructInstruction deconstruction + && singleGetter == deconstruction.Pattern.TestedOperand + && CanBeDeconstructedInForeach(deconstruction, usingContainer, loopContainer)) + { + return RequiredGetCurrentTransformation.Deconstruction; + } ILInstruction inst = singleGetter; // in some cases, i.e. foreach variable with explicit type different from the collection-item-type, // the result of call get_Current is casted. @@ -738,7 +795,7 @@ namespace ICSharpCode.Decompiler.CSharp // One variable was found. if (inst.Parent is StLoc stloc && (stloc.Variable.Kind == VariableKind.Local || stloc.Variable.Kind == VariableKind.StackSlot)) { // Must be a plain assignment expression and variable must only be used in 'body' + only assigned once. - if (stloc.Parent == loopBody && VariableIsOnlyUsedInBlock(stloc, usingContainer)) { + if (stloc.Parent == loopBody && VariableIsOnlyUsedInBlock(stloc, usingContainer, loopContainer)) { foreachVariable = stloc.Variable; return RequiredGetCurrentTransformation.UseExistingVariable; } @@ -752,13 +809,42 @@ namespace ICSharpCode.Decompiler.CSharp return RequiredGetCurrentTransformation.IntroduceNewVariable; } + bool CanBeDeconstructedInForeach(DeconstructInstruction deconstruction, BlockContainer usingContainer, BlockContainer loopContainer) + { + if (deconstruction.Init.Count > 0) + return false; + if (deconstruction.Conversions.Instructions.Count > 0) + return false; + var operandType = deconstruction.Pattern.TestedOperand.InferType(this.typeSystem); + var expectedType = deconstruction.Pattern.Variable.Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(operandType, expectedType)) + return false; + var usedVariables = new HashSet(ILVariableEqualityComparer.Instance); + foreach (var item in deconstruction.Assignments.Instructions) { + if (!item.MatchStLoc(out var v, out var value)) + return false; + expectedType = ((LdLoc)value).Variable.Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(v.Type, expectedType)) + return false; + if (!(v.Kind == VariableKind.StackSlot || v.Kind == VariableKind.Local)) + return false; + if (!VariableIsOnlyUsedInBlock((StLoc)item, usingContainer, loopContainer)) + return false; + if (!(v.CaptureScope == null || v.CaptureScope == usingContainer)) + return false; + if (!usedVariables.Add(v)) + return false; + } + return true; + } + /// /// Determines whether storeInst.Variable is only assigned once and used only inside . /// Loads by reference (ldloca) are only allowed in the context of this pointer in call instructions, /// or as target of ldobj. /// (This only applies to value types.) /// - bool VariableIsOnlyUsedInBlock(StLoc storeInst, BlockContainer usingContainer) + bool VariableIsOnlyUsedInBlock(StLoc storeInst, BlockContainer usingContainer, BlockContainer loopContainer) { if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(usingContainer))) return false; @@ -766,6 +852,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (storeInst.Variable.StoreInstructions.OfType().Any(st => st != storeInst)) return false; + if (!(storeInst.Variable.CaptureScope == null || storeInst.Variable.CaptureScope == loopContainer)) + return false; return true; bool AddressUseAllowed(LdLoca la) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index f543cf611..610fabbe3 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -501,9 +501,14 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax VisitChildren (objectCreateExpression); } - public virtual void VisitOutVarDeclarationExpression (OutVarDeclarationExpression outVarDeclarationExpression) + public virtual void VisitDeclarationExpression(DeclarationExpression declarationExpression) { - VisitChildren (outVarDeclarationExpression); + VisitChildren(declarationExpression); + } + + public virtual void VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) + { + VisitChildren(outVarDeclarationExpression); } public virtual void VisitAnonymousTypeCreateExpression(AnonymousTypeCreateExpression anonymousTypeCreateExpression) @@ -666,6 +671,16 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax VisitChildren (namedExpression); } + public virtual void VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation) + { + VisitChildren(singleVariableDesignation); + } + + public virtual void VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation) + { + VisitChildren(parenthesizedVariableDesignation); + } + public virtual void VisitErrorNode(AstNode errorNode) { VisitChildren(errorNode); @@ -1153,7 +1168,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren (objectCreateExpression); } - public virtual T VisitOutVarDeclarationExpression (OutVarDeclarationExpression outVarDeclarationExpression) + public virtual T VisitDeclarationExpression(DeclarationExpression declarationExpression) + { + return VisitChildren(declarationExpression); + } + + public virtual T VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression) { return VisitChildren(outVarDeclarationExpression); } @@ -1318,6 +1338,16 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren (namedExpression); } + public virtual T VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation) + { + return VisitChildren(singleVariableDesignation); + } + + public virtual T VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation) + { + return VisitChildren(parenthesizedVariableDesignation); + } + public virtual T VisitErrorNode(AstNode errorNode) { return VisitChildren(errorNode); @@ -1805,9 +1835,14 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren (objectCreateExpression, data); } + public virtual S VisitDeclarationExpression(DeclarationExpression declarationExpression, T data) + { + return VisitChildren(declarationExpression, data); + } + public virtual S VisitOutVarDeclarationExpression(OutVarDeclarationExpression outVarDeclarationExpression, T data) { - return VisitChildren (outVarDeclarationExpression, data); + return VisitChildren(outVarDeclarationExpression, data); } public virtual S VisitAnonymousTypeCreateExpression(AnonymousTypeCreateExpression anonymousTypeCreateExpression, T data) @@ -1970,6 +2005,16 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren (namedExpression, data); } + public virtual S VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation, T data) + { + return VisitChildren(singleVariableDesignation, data); + } + + public virtual S VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation, T data) + { + return VisitChildren(parenthesizedVariableDesignation, data); + } + public virtual S VisitErrorNode(AstNode errorNode, T data) { return VisitChildren(errorNode, data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs new file mode 100644 index 000000000..0e8585233 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2020 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. + +using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + /// + /// TypeName VariableDesignation + /// + public class DeclarationExpression : Expression + { + public AstType Type { + get { return GetChildByRole(Roles.Type); } + set { SetChildByRole(Roles.Type, value); } + } + + public VariableDesignation Designation { + get { return GetChildByRole(Roles.VariableDesignationRole); } + set { SetChildByRole(Roles.VariableDesignationRole, value); } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitDeclarationExpression(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitDeclarationExpression(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitDeclarationExpression(this, data); + } + + protected internal override bool DoMatch(AstNode other, Match match) + { + return other is DeclarationExpression o && Designation.DoMatch(o.Designation, match); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/IdentifierExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/IdentifierExpression.cs index fd3690ed2..0c5839886 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/IdentifierExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/IdentifierExpression.cs @@ -42,10 +42,6 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax SetChildByRole(Roles.Identifier, Decompiler.CSharp.Syntax.Identifier.Create (identifier, location)); } -// public Identifier IdentifierToken { -// get { return GetChildByRole (Roles.Identifier); } -// } - public string Identifier { get { return GetChildByRole (Roles.Identifier).Name; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs index ef73b991d..701041316 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs @@ -35,6 +35,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax void VisitCastExpression(CastExpression castExpression); void VisitCheckedExpression(CheckedExpression checkedExpression); void VisitConditionalExpression(ConditionalExpression conditionalExpression); + void VisitDeclarationExpression(DeclarationExpression declarationExpression); void VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression); void VisitDirectionExpression(DirectionExpression directionExpression); void VisitIdentifierExpression(IdentifierExpression identifierExpression); @@ -155,7 +156,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax void VisitInterpolation(Interpolation interpolation); void VisitInterpolatedStringText(InterpolatedStringText interpolatedStringText); - + + void VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation); + void VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation); + void VisitNullNode(AstNode nullNode); void VisitErrorNode(AstNode errorNode); void VisitPatternPlaceholder(AstNode placeholder, PatternMatching.Pattern pattern); @@ -177,6 +181,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitCastExpression(CastExpression castExpression); S VisitCheckedExpression(CheckedExpression checkedExpression); S VisitConditionalExpression(ConditionalExpression conditionalExpression); + S VisitDeclarationExpression(DeclarationExpression declarationExpression); S VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression); S VisitDirectionExpression(DirectionExpression directionExpression); S VisitIdentifierExpression(IdentifierExpression identifierExpression); @@ -298,6 +303,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitInterpolation(Interpolation interpolation); S VisitInterpolatedStringText(InterpolatedStringText interpolatedStringText); + S VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation); + S VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation); + S VisitNullNode(AstNode nullNode); S VisitErrorNode(AstNode errorNode); S VisitPatternPlaceholder(AstNode placeholder, PatternMatching.Pattern pattern); @@ -319,6 +327,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitCastExpression(CastExpression castExpression, T data); S VisitCheckedExpression(CheckedExpression checkedExpression, T data); S VisitConditionalExpression(ConditionalExpression conditionalExpression, T data); + S VisitDeclarationExpression(DeclarationExpression declarationExpression, T data); S VisitDefaultValueExpression(DefaultValueExpression defaultValueExpression, T data); S VisitDirectionExpression(DirectionExpression directionExpression, T data); S VisitIdentifierExpression(IdentifierExpression identifierExpression, T data); @@ -440,6 +449,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitInterpolation(Interpolation interpolation, T data); S VisitInterpolatedStringText(InterpolatedStringText interpolatedStringText, T data); + S VisitSingleVariableDesignation(SingleVariableDesignation singleVariableDesignation, T data); + S VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation, T data); + S VisitNullNode(AstNode nullNode, T data); S VisitErrorNode(AstNode errorNode, T data); S VisitPatternPlaceholder(AstNode placeholder, PatternMatching.Pattern pattern, T data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index 872f977aa..b45935814 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -46,11 +46,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly Role Variable = new Role ("Variable", VariableInitializer.Null); public static readonly Role EmbeddedStatement = new Role ("EmbeddedStatement", Statement.Null); public readonly static Role TypeMemberRole = new Role ("TypeMember"); - + + public static readonly Role VariableDesignationRole = new Role("VariableDesignation", VariableDesignation.Null); // public static readonly TokenRole Keyword = new TokenRole ("Keyword", CSharpTokenNode.Null); -// public static readonly TokenRole InKeyword = new TokenRole ("InKeyword", CSharpTokenNode.Null); - + // public static readonly TokenRole InKeyword = new TokenRole ("InKeyword", CSharpTokenNode.Null); + // some pre defined constants for most used punctuation public static readonly TokenRole LPar = new TokenRole ("("); public static readonly TokenRole RPar = new TokenRole (")"); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs index 4c1d037c2..d2bd3f3d3 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs @@ -47,23 +47,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax get { return GetChildByRole (Roles.Type); } set { SetChildByRole (Roles.Type, value); } } - - public string VariableName { - get { - return GetChildByRole (Roles.Identifier).Name; - } - set { - SetChildByRole(Roles.Identifier, Identifier.Create (value)); - } - } - - public Identifier VariableNameToken { - get { - return GetChildByRole (Roles.Identifier); - } - set { - SetChildByRole(Roles.Identifier, value); - } + + public VariableDesignation VariableDesignation { + get { return GetChildByRole(Roles.VariableDesignationRole); } + set { SetChildByRole(Roles.VariableDesignationRole, value); } } public CSharpTokenNode InToken { @@ -102,7 +89,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) { ForeachStatement o = other as ForeachStatement; - return o != null && this.VariableType.DoMatch(o.VariableType, match) && MatchString(this.VariableName, o.VariableName) + return o != null && this.VariableType.DoMatch(o.VariableType, match) && this.VariableDesignation.DoMatch(o.VariableDesignation, match) && this.InExpression.DoMatch(o.InExpression, match) && this.EmbeddedStatement.DoMatch(o.EmbeddedStatement, match); } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs b/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs new file mode 100644 index 000000000..8d7991dec --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2020 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. + +using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + public abstract class VariableDesignation : AstNode + { + public override NodeType NodeType => NodeType.Unknown; + + #region Null + public new static readonly VariableDesignation Null = new NullVariableDesignation(); + + sealed class NullVariableDesignation : VariableDesignation + { + public override bool IsNull { + get { + return true; + } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitNullNode(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitNullNode(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitNullNode(this, data); + } + + protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) + { + return other == null || other.IsNull; + } + } + #endregion + + } + + /// + /// Identifier + /// + public class SingleVariableDesignation : VariableDesignation + { + + public string Identifier { + get { return GetChildByRole(Roles.Identifier).Name; } + set { SetChildByRole(Roles.Identifier, Syntax.Identifier.Create(value)); } + } + + public Identifier IdentifierToken { + get { return GetChildByRole(Roles.Identifier); } + set { SetChildByRole(Roles.Identifier, value); } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitSingleVariableDesignation(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitSingleVariableDesignation(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitSingleVariableDesignation(this, data); + } + + protected internal override bool DoMatch(AstNode other, Match match) + { + return other is SingleVariableDesignation o && MatchString(this.Identifier, o.Identifier); + } + } + + /// + /// ( VariableDesignation (, VariableDesignation)* ) + /// + public class ParenthesizedVariableDesignation : VariableDesignation + { + + public CSharpTokenNode LParToken { + get { return GetChildByRole(Roles.LPar); } + } + + public AstNodeCollection VariableDesignations { + get { return GetChildrenByRole(Roles.VariableDesignationRole); } + } + + public CSharpTokenNode RParToken { + get { return GetChildByRole(Roles.RPar); } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitParenthesizedVariableDesignation(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitParenthesizedVariableDesignation(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitParenthesizedVariableDesignation(this, data); + } + + protected internal override bool DoMatch(AstNode other, Match match) + { + return other is ParenthesizedVariableDesignation o && VariableDesignations.DoMatch(o.VariableDesignations, match); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index ba79f6709..b7f1932c9 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -102,6 +102,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public VariableToDeclare ReplacementDueToCollision; public bool InvolvedInCollision; public bool RemovedDueToCollision => ReplacementDueToCollision != null; + public bool DeclaredInDeconstruction; public VariableToDeclare(ILVariable variable, bool defaultInitialization, InsertionPoint insertionPoint, IdentifierExpression firstUse, int sourceOrder) { @@ -126,6 +127,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms EnsureExpressionStatementsAreValid(rootNode); FindInsertionPoints(rootNode, 0); ResolveCollisions(); + InsertDeconstructionVariableDeclarations(); InsertVariableDeclarations(context); UpdateAnnotations(rootNode); } finally { @@ -133,7 +135,6 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms variableDict.Clear(); } } - /// /// Analyze the input AST (containing undeclared variables) /// for where those variables would be declared by this transform. @@ -425,6 +426,44 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } } + private void InsertDeconstructionVariableDeclarations() + { + var usedVariables = new HashSet(); + foreach (var g in variableDict.Values.GroupBy(v => v.InsertionPoint.nextNode)) { + if (!(g.Key is ExpressionStatement { Expression: AssignmentExpression { Left: TupleExpression left, Operator: AssignmentOperatorType.Assign } assignment })) + continue; + usedVariables.Clear(); + var deconstruct = assignment.Annotation(); + if (deconstruct == null || deconstruct.Init.Count > 0 || deconstruct.Conversions.Instructions.Count > 0) + continue; + if (!deconstruct.Assignments.Instructions.All(IsDeclarableVariable)) + continue; + + var designation = StatementBuilder.TranslateDeconstructionDesignation(deconstruct, isForeach: false); + left.ReplaceWith(new DeclarationExpression { Type = new SimpleType("var"), Designation = designation }); + + foreach (var v in usedVariables) { + variableDict[v].DeclaredInDeconstruction = true; + } + + bool IsDeclarableVariable(ILInstruction inst) + { + if (!inst.MatchStLoc(out var v, out var value)) + return false; + if (!g.Any(vd => vd.ILVariable == v && !vd.RemovedDueToCollision)) + return false; + if (!usedVariables.Add(v)) + return false; + var expectedType = ((LdLoc)value).Variable.Type; + if (!v.Type.Equals(expectedType)) + return false; + if (!(v.Kind == VariableKind.StackSlot || v.Kind == VariableKind.Local)) + return false; + return true; + } + } + } + bool IsMatchingAssignment(VariableToDeclare v, out AssignmentExpression assignment) { assignment = v.InsertionPoint.nextNode as AssignmentExpression; @@ -454,7 +493,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { var replacements = new List<(AstNode, AstNode)>(); foreach (var (ilVariable, v) in variableDict) { - if (v.RemovedDueToCollision) + if (v.RemovedDueToCollision || v.DeclaredInDeconstruction) continue; if (CombineDeclarationAndInitializer(v, context) && IsMatchingAssignment(v, out AssignmentExpression assignment)) { diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index 899934023..c65d02d75 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -337,14 +337,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms body.Statements.Add(statement.Detach()); var foreachStmt = new ForeachStatement { VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type), - VariableName = itemVariable.Name, + VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name }, InExpression = m.Get("arrayVariable").Single().Detach(), EmbeddedStatement = body }; foreachStmt.CopyAnnotationsFrom(forStatement); itemVariable.Kind = IL.VariableKind.ForeachLocal; // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); + foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation forStatement.ReplaceWith(foreachStmt); return foreachStmt; @@ -495,7 +495,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms body.Statements.Add(statement.Detach()); var foreachStmt = new ForeachStatement { VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type), - VariableName = itemVariable.Name, + VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name }, InExpression = m.Get("collection").Single().Detach(), EmbeddedStatement = body }; @@ -504,7 +504,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms //foreachStmt.CopyAnnotationsFrom(forStatement); itemVariable.Kind = IL.VariableKind.ForeachLocal; // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); + foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation expressionStatement.ReplaceWith(foreachStmt); return foreachStmt; diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index df41aa008..fa80261cd 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -93,6 +93,7 @@ namespace ICSharpCode.Decompiler tupleConversions = false; discards = false; localFunctions = false; + deconstruction = false; } if (languageVersion < CSharp.LanguageVersion.CSharp7_2) { introduceReadonlyAndInModifiers = false; @@ -1162,6 +1163,23 @@ namespace ICSharpCode.Decompiler } } + bool deconstruction = true; + + /// + /// Gets/Sets whether C# 7.0 deconstruction should be detected. + /// + [Category("C# 7.0 / VS 2017")] + [Description("DecompilerSettings.Deconstruction")] + public bool Deconstruction { + get { return deconstruction; } + set { + if (deconstruction != value) { + deconstruction = value; + OnPropertyChanged(); + } + } + } + bool staticLocalFunctions = true; /// @@ -1182,7 +1200,7 @@ namespace ICSharpCode.Decompiler bool ranges = true; /// - /// Gets/Sets whether C# 8.0 static local functions should be transformed. + /// Gets/Sets whether C# 8.0 index and range syntax should be used. /// [Category("C# 8.0 / VS 2019")] [Description("DecompilerSettings.Ranges")] diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs index a43ac19e6..ed5d01918 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs @@ -646,6 +646,8 @@ namespace ICSharpCode.Decompiler.FlowAnalysis } else { return (state, bottomState.Clone()); } + } else if (inst is MatchInstruction match) { + return EvaluateMatch(match); } else { // other kind of condition inst.AcceptVisitor(this); @@ -653,6 +655,45 @@ namespace ICSharpCode.Decompiler.FlowAnalysis } } + protected internal override void VisitMatchInstruction(MatchInstruction inst) + { + var (onTrue, onFalse) = EvaluateMatch(inst); + state = onTrue; + state.JoinWith(onFalse); + } + + /// + /// Evaluates a match instruction. + /// + /// + /// A pair of: + /// * The state after the pattern matches + /// * The state after the pattern fails to match + /// + /// + /// this.state is invalid after this function was called, and must be overwritten + /// with one of the return values. + /// + (State OnTrue, State OnFalse) EvaluateMatch(MatchInstruction inst) + { + DebugStartPoint(inst); + inst.TestedOperand.AcceptVisitor(this); + State onFalse = state.Clone(); + if (!inst.CheckNotNull && !inst.CheckType) { + onFalse.ReplaceWithBottom(); + } + HandleMatchStore(inst); + foreach (var subPattern in inst.SubPatterns) { + var (subTrue, subFalse) = EvaluateCondition(subPattern); + onFalse.JoinWith(subFalse); + state = subTrue; + } + DebugEndPoint(inst); + return (state, onFalse); + } + + protected abstract void HandleMatchStore(MatchInstruction inst); + protected internal override void VisitNullCoalescingInstruction(NullCoalescingInstruction inst) { HandleBinaryWithOptionalEvaluation(inst, inst.ValueInst, inst.FallbackInst); diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs index 5b465b735..726cef1f7 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs @@ -168,7 +168,12 @@ namespace ICSharpCode.Decompiler.FlowAnalysis inst.Value.AcceptVisitor(this); HandleStore(inst.Variable); } - + + protected override void HandleMatchStore(MatchInstruction inst) + { + HandleStore(inst.Variable); + } + protected override void BeginTryCatchHandler(TryCatchHandler inst) { HandleStore(inst.Variable); diff --git a/ICSharpCode.Decompiler/FlowAnalysis/ReachingDefinitionsVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/ReachingDefinitionsVisitor.cs index 8b6d22a24..08bf2e4f2 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/ReachingDefinitionsVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/ReachingDefinitionsVisitor.cs @@ -380,7 +380,12 @@ namespace ICSharpCode.Decompiler.FlowAnalysis inst.Value.AcceptVisitor(this); HandleStore(inst, inst.Variable); } - + + protected override void HandleMatchStore(MatchInstruction inst) + { + HandleStore(inst, inst.Variable); + } + protected override void BeginTryCatchHandler(TryCatchHandler inst) { base.BeginTryCatchHandler(inst); diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index e10626114..b8ae5a34d 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -70,9 +70,15 @@ + + + + + + @@ -634,13 +640,25 @@ + + + powershell -NoProfile -ExecutionPolicy Bypass -File BuildTools/update-assemblyinfo.ps1 $(Configuration) + git rev-parse HEAD^^{commit} pwsh -NoProfile -ExecutionPolicy Bypass -File BuildTools/update-assemblyinfo.ps1 $(Configuration) + git rev-parse HEAD^{commit} - + + + + + + + + \ No newline at end of file diff --git a/ICSharpCode.Decompiler/IL/ILVariable.cs b/ICSharpCode.Decompiler/IL/ILVariable.cs index 607f0b47b..fb833467d 100644 --- a/ICSharpCode.Decompiler/IL/ILVariable.cs +++ b/ICSharpCode.Decompiler/IL/ILVariable.cs @@ -75,6 +75,14 @@ namespace ICSharpCode.Decompiler.IL /// Local variable that holds the display class used for lambdas within this function. /// DisplayClassLocal, + /// + /// Local variable declared within a pattern match. + /// + PatternLocal, + /// + /// Temporary variable declared in a deconstruction init section. + /// + DeconstructionInitTemporary, } static class VariableKindExtensions @@ -433,6 +441,12 @@ namespace ICSharpCode.Decompiler.IL case VariableKind.DisplayClassLocal: output.Write("display_class local "); break; + case VariableKind.PatternLocal: + output.Write("pattern local "); + break; + case VariableKind.DeconstructionInitTemporary: + output.Write("deconstruction init temporary "); + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index a292afb07..66af1937e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -220,6 +220,8 @@ namespace ICSharpCode.Decompiler.IL DynamicInvokeInstruction, /// ILAst representation of a call to the Binder.IsEvent method inside a dynamic expression. DynamicIsEventInstruction, + /// ILAst representation of C# patterns + MatchInstruction, /// Push a typed reference of type class onto the stack. MakeRefAny, /// Push the type token stored in a typed reference. @@ -230,6 +232,10 @@ namespace ICSharpCode.Decompiler.IL YieldReturn, /// C# await operator. Await, + /// Deconstruction statement + DeconstructInstruction, + /// Represents a deconstructed value + DeconstructResultInstruction, /// Matches any node AnyNode, } @@ -6085,6 +6091,147 @@ namespace ICSharpCode.Decompiler.IL } } namespace ICSharpCode.Decompiler.IL +{ + /// ILAst representation of C# patterns + public sealed partial class MatchInstruction : ILInstruction, IStoreInstruction, IInstructionWithMethodOperand + { + public MatchInstruction(ILVariable variable, IMethod method, ILInstruction testedOperand, params ILInstruction[] subPatterns) : base(OpCode.MatchInstruction) + { + Debug.Assert(variable != null); + this.variable = variable; + this.method = method; + this.TestedOperand = testedOperand; + this.SubPatterns = new InstructionCollection(this, 1); + this.SubPatterns.AddRange(subPatterns); + } + ILVariable variable; + public ILVariable Variable { + get { return variable; } + set { + Debug.Assert(value != null); + if (IsConnected) + variable.RemoveStoreInstruction(this); + variable = value; + if (IsConnected) + variable.AddStoreInstruction(this); + } + } + + public int IndexInStoreInstructionList { get; set; } = -1; + + int IInstructionWithVariableOperand.IndexInVariableInstructionMapping { + get { return ((IStoreInstruction)this).IndexInStoreInstructionList; } + set { ((IStoreInstruction)this).IndexInStoreInstructionList = value; } + } + + protected override void Connected() + { + base.Connected(); + variable.AddStoreInstruction(this); + } + + protected override void Disconnected() + { + variable.RemoveStoreInstruction(this); + base.Disconnected(); + } + + readonly IMethod method; + /// Returns the method operand. + public IMethod Method { get { return method; } } + public bool IsDeconstructCall; + public bool IsDeconstructTuple; + public bool CheckType; + public bool CheckNotNull; + public static readonly SlotInfo TestedOperandSlot = new SlotInfo("TestedOperand", canInlineInto: true); + ILInstruction testedOperand; + public ILInstruction TestedOperand { + get { return this.testedOperand; } + set { + ValidateChild(value); + SetChildInstruction(ref this.testedOperand, value, 0); + } + } + public static readonly SlotInfo SubPatternsSlot = new SlotInfo("SubPatterns"); + public InstructionCollection SubPatterns { get; private set; } + protected sealed override int GetChildCount() + { + return 1 + SubPatterns.Count; + } + protected sealed override ILInstruction GetChild(int index) + { + switch (index) { + case 0: + return this.testedOperand; + default: + return this.SubPatterns[index - 1]; + } + } + protected sealed override void SetChild(int index, ILInstruction value) + { + switch (index) { + case 0: + this.TestedOperand = value; + break; + default: + this.SubPatterns[index - 1] = (ILInstruction)value; + break; + } + } + protected sealed override SlotInfo GetChildSlot(int index) + { + switch (index) { + case 0: + return TestedOperandSlot; + default: + return SubPatternsSlot; + } + } + public sealed override ILInstruction Clone() + { + var clone = (MatchInstruction)ShallowClone(); + clone.TestedOperand = this.testedOperand.Clone(); + clone.SubPatterns = new InstructionCollection(clone, 1); + clone.SubPatterns.AddRange(this.SubPatterns.Select(arg => (ILInstruction)arg.Clone())); + return clone; + } + public override StackType ResultType { get { return StackType.I4; } } + protected override InstructionFlags ComputeFlags() + { + return InstructionFlags.MayWriteLocals | testedOperand.Flags | SubPatterns.Aggregate(InstructionFlags.None, (f, arg) => f | arg.Flags) | InstructionFlags.SideEffect | InstructionFlags.MayThrow | InstructionFlags.ControlFlow; + } + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.MayWriteLocals | InstructionFlags.SideEffect | InstructionFlags.MayThrow | InstructionFlags.ControlFlow; + } + } + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitMatchInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitMatchInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitMatchInstruction(this, context); + } + protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) + { + var o = other as MatchInstruction; + return o != null && variable == o.variable && object.Equals(method, o.method) && this.IsDeconstructCall == o.IsDeconstructCall && this.IsDeconstructTuple == o.IsDeconstructTuple && this.CheckType == o.CheckType && this.CheckNotNull == o.CheckNotNull && this.testedOperand.PerformMatch(o.testedOperand, ref match) && Patterns.ListMatch.DoMatch(this.SubPatterns, o.SubPatterns, ref match); + } + internal override void CheckInvariant(ILPhase phase) + { + base.CheckInvariant(phase); + Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function)); + Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable); + AdditionalInvariants(); + } + } +} +namespace ICSharpCode.Decompiler.IL { /// Push a typed reference of type class onto the stack. public sealed partial class MakeRefAny : UnaryInstruction @@ -6397,6 +6544,61 @@ namespace ICSharpCode.Decompiler.IL } } } +namespace ICSharpCode.Decompiler.IL +{ + /// Deconstruction statement + public sealed partial class DeconstructInstruction : ILInstruction + { + public override StackType ResultType { get { return StackType.Void; } } + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitDeconstructInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitDeconstructInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitDeconstructInstruction(this, context); + } + protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) + { + var o = other as DeconstructInstruction; + return o != null; + } + } +} +namespace ICSharpCode.Decompiler.IL +{ + /// Represents a deconstructed value + public sealed partial class DeconstructResultInstruction : UnaryInstruction + { + + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitDeconstructResultInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitDeconstructResultInstruction(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitDeconstructResultInstruction(this, context); + } + protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) + { + var o = other as DeconstructResultInstruction; + return o != null && this.Argument.PerformMatch(o.Argument, ref match); + } + internal override void CheckInvariant(ILPhase phase) + { + base.CheckInvariant(phase); + AdditionalInvariants(); + } + } +} namespace ICSharpCode.Decompiler.IL.Patterns { /// Matches any node @@ -6808,6 +7010,10 @@ namespace ICSharpCode.Decompiler.IL { Default(inst); } + protected internal virtual void VisitMatchInstruction(MatchInstruction inst) + { + Default(inst); + } protected internal virtual void VisitMakeRefAny(MakeRefAny inst) { Default(inst); @@ -6828,6 +7034,14 @@ namespace ICSharpCode.Decompiler.IL { Default(inst); } + protected internal virtual void VisitDeconstructInstruction(DeconstructInstruction inst) + { + Default(inst); + } + protected internal virtual void VisitDeconstructResultInstruction(DeconstructResultInstruction inst) + { + Default(inst); + } } /// @@ -7194,6 +7408,10 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst); } + protected internal virtual T VisitMatchInstruction(MatchInstruction inst) + { + return Default(inst); + } protected internal virtual T VisitMakeRefAny(MakeRefAny inst) { return Default(inst); @@ -7214,6 +7432,14 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst); } + protected internal virtual T VisitDeconstructInstruction(DeconstructInstruction inst) + { + return Default(inst); + } + protected internal virtual T VisitDeconstructResultInstruction(DeconstructResultInstruction inst) + { + return Default(inst); + } } /// @@ -7580,6 +7806,10 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst, context); } + protected internal virtual T VisitMatchInstruction(MatchInstruction inst, C context) + { + return Default(inst, context); + } protected internal virtual T VisitMakeRefAny(MakeRefAny inst, C context) { return Default(inst, context); @@ -7600,6 +7830,14 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst, context); } + protected internal virtual T VisitDeconstructInstruction(DeconstructInstruction inst, C context) + { + return Default(inst, context); + } + protected internal virtual T VisitDeconstructResultInstruction(DeconstructResultInstruction inst, C context) + { + return Default(inst, context); + } } partial class InstructionOutputExtensions @@ -7694,11 +7932,14 @@ namespace ICSharpCode.Decompiler.IL "dynamic.invokeconstructor", "dynamic.invoke", "dynamic.isevent", + "match", "mkrefany", "refanytype", "refanyval", "yield.return", "await", + "deconstruct", + "deconstruct.result", "AnyNode", }; } @@ -8255,6 +8496,20 @@ namespace ICSharpCode.Decompiler.IL right = default(ILInstruction); return false; } + public bool MatchMatchInstruction(out ILVariable variable, out IMethod method, out ILInstruction testedOperand) + { + var inst = this as MatchInstruction; + if (inst != null) { + variable = inst.Variable; + method = inst.Method; + testedOperand = inst.TestedOperand; + return true; + } + variable = default(ILVariable); + method = default(IMethod); + testedOperand = default(ILInstruction); + return false; + } public bool MatchMakeRefAny(out ILInstruction argument, out IType type) { var inst = this as MakeRefAny; diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 92b30e6f9..627733750 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -335,6 +335,13 @@ new OpCode("dynamic.isevent", "ILAst representation of a call to the Binder.IsEvent method inside a dynamic expression.", CustomClassName("DynamicIsEventInstruction"), Dynamic, CustomArguments(("argument", new[] { "O" })), CustomWriteTo), + new OpCode("match", "ILAst representation of C# patterns", + CustomClassName("MatchInstruction"), HasVariableOperand("Store"), HasMethodOperand, + BoolFlag("IsDeconstructCall"), BoolFlag("IsDeconstructTuple"), BoolFlag("CheckType"), BoolFlag("CheckNotNull"), + CustomChildren(new []{ + new ChildInfo("testedOperand") { CanInlineInto = true }, + new ChildInfo("subPatterns") { IsCollection = true } + }), ResultType("I4"), CustomWriteTo, SideEffect, MayThrow, ControlFlow, CustomInvariant("AdditionalInvariants();")), new OpCode("mkrefany", "Push a typed reference of type class onto the stack.", CustomClassName("MakeRefAny"), Unary, HasTypeOperand, ResultType("O")), @@ -352,6 +359,12 @@ SideEffect, // other code can run with arbitrary side effects while we're waiting CustomArguments(("value", null)), ResultType("GetResultMethod?.ReturnType.GetStackType() ?? StackType.Unknown")), + new OpCode("deconstruct", "Deconstruction statement", + CustomClassName("DeconstructInstruction"), CustomConstructor, ResultType("Void"), CustomWriteTo), + new OpCode("deconstruct.result", "Represents a deconstructed value", + CustomClassName("DeconstructResultInstruction"), CustomConstructor, CustomInvariant("AdditionalInvariants();"), + Unary, CustomWriteTo), + // patterns new OpCode("AnyNode", "Matches any node", Pattern, CustomArguments(), CustomConstructor), }; diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index c7a029506..99fe130ea 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -171,6 +171,12 @@ namespace ICSharpCode.Decompiler.IL Debug.Assert(Instructions[i] is StLoc || AccessPathElement.GetAccessPath(Instructions[i], type2).Kind != IL.Transforms.AccessPathKind.Invalid); } break; + case BlockKind.DeconstructionConversions: + Debug.Assert(this.SlotInfo == DeconstructInstruction.ConversionsSlot); + break; + case BlockKind.DeconstructionAssignments: + Debug.Assert(this.SlotInfo == DeconstructInstruction.AssignmentsSlot); + break; } } @@ -420,5 +426,13 @@ namespace ICSharpCode.Decompiler.IL /// } /// CallWithNamedArgs, + /// + /// + /// + DeconstructionConversions, + /// + /// + /// + DeconstructionAssignments, } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs new file mode 100644 index 000000000..62ba9aae1 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs @@ -0,0 +1,300 @@ +// Copyright (c) 2020 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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL +{ + partial class DeconstructInstruction + { + public static readonly SlotInfo InitSlot = new SlotInfo("Init", canInlineInto: true, isCollection: true); + public static readonly SlotInfo PatternSlot = new SlotInfo("Pattern", canInlineInto: true); + public static readonly SlotInfo ConversionsSlot = new SlotInfo("Conversions"); + public static readonly SlotInfo AssignmentsSlot = new SlotInfo("Assignments"); + + public DeconstructInstruction() + : base(OpCode.DeconstructInstruction) + { + this.Init = new InstructionCollection(this, 0); + } + + public readonly InstructionCollection Init; + + MatchInstruction pattern; + public MatchInstruction Pattern { + get { return this.pattern; } + set { + ValidateChild(value); + SetChildInstruction(ref this.pattern, value, Init.Count); + } + } + + Block conversions; + public Block Conversions { + get { return this.conversions; } + set { + ValidateChild(value); + SetChildInstruction(ref this.conversions, value, Init.Count + 1); + } + } + + Block assignments; + public Block Assignments { + get { return this.assignments; } + set { + ValidateChild(value); + SetChildInstruction(ref this.assignments, value, Init.Count + 2); + } + } + + protected sealed override int GetChildCount() + { + return Init.Count + 3; + } + + protected sealed override ILInstruction GetChild(int index) + { + switch (index - Init.Count) { + case 0: + return this.pattern; + case 1: + return this.conversions; + case 2: + return this.assignments; + default: + return this.Init[index]; + } + } + + protected sealed override void SetChild(int index, ILInstruction value) + { + switch (index - Init.Count) { + case 0: + this.Pattern = (MatchInstruction)value; + break; + case 1: + this.Conversions = (Block)value; + break; + case 2: + this.Assignments = (Block)value; + break; + default: + this.Init[index] = (StLoc)value; + break; + } + } + + protected sealed override SlotInfo GetChildSlot(int index) + { + switch (index - Init.Count) { + case 0: + return PatternSlot; + case 1: + return ConversionsSlot; + case 2: + return AssignmentsSlot; + default: + return InitSlot; + } + } + + public sealed override ILInstruction Clone() + { + var clone = new DeconstructInstruction(); + clone.Init.AddRange(this.Init.Select(inst => (StLoc)inst.Clone())); + clone.Pattern = (MatchInstruction)this.pattern.Clone(); + clone.Conversions = (Block)this.conversions.Clone(); + clone.Assignments = (Block)this.assignments.Clone(); + return clone; + } + + protected override InstructionFlags ComputeFlags() + { + var flags = InstructionFlags.None; + foreach (var inst in Init) { + flags |= inst.Flags; + } + flags |= pattern.Flags | conversions.Flags | assignments.Flags; + return flags; + } + + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.None; + } + } + + protected internal override void InstructionCollectionUpdateComplete() + { + base.InstructionCollectionUpdateComplete(); + if (pattern.Parent == this) + pattern.ChildIndex = Init.Count; + if (conversions.Parent == this) + conversions.ChildIndex = Init.Count + 1; + if (assignments.Parent == this) + assignments.ChildIndex = Init.Count + 2; + } + + public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + { + WriteILRange(output, options); + output.Write("deconstruct "); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + output.WriteLine("init:"); + output.Indent(); + foreach (var inst in this.Init) { + inst.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.WriteLine("pattern:"); + output.Indent(); + pattern.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("conversions: "); + conversions.WriteTo(output, options); + output.WriteLine(); + output.Write("assignments: "); + assignments.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write('}'); + output.MarkFoldEnd(); + } + + internal static bool IsConversionStLoc(ILInstruction inst, out ILVariable variable, out ILVariable inputVariable) + { + inputVariable = null; + if (!inst.MatchStLoc(out variable, out var value)) + return false; + ILInstruction input; + switch (value) { + case Conv conv: + input = conv.Argument; + break; + //case Call { Method: { IsOperator: true, Name: "op_Implicit" }, Arguments: { Count: 1 } } call: + // input = call.Arguments[0]; + // break; + default: + return false; + } + return input.MatchLdLoc(out inputVariable) || input.MatchLdLoca(out inputVariable); + } + + internal static bool IsAssignment(ILInstruction inst, ICompilation typeSystem, out IType expectedType, out ILInstruction value) + { + expectedType = null; + value = null; + switch (inst) { + case CallInstruction call: + if (call.Method.AccessorKind != System.Reflection.MethodSemanticsAttributes.Setter) + return false; + for (int i = 0; i < call.Arguments.Count - 1; i++) { + ILInstruction arg = call.Arguments[i]; + if (arg.Flags == InstructionFlags.None) { + // OK - we accept integer literals, etc. + } else if (arg.MatchLdLoc(out var v)) { + } else { + return false; + } + } + expectedType = call.Method.Parameters.Last().Type; + value = call.Arguments.Last(); + return true; + case StLoc stloc: + expectedType = stloc.Variable.Type; + value = stloc.Value; + return true; + case StObj stobj: + var target = stobj.Target; + while (target.MatchLdFlda(out var nestedTarget, out _)) + target = nestedTarget; + if (target.Flags == InstructionFlags.None) { + // OK - we accept integer literals, etc. + } else if (target.MatchLdLoc(out var v)) { + } else { + return false; + } + if (stobj.Target.InferType(typeSystem) is ByReferenceType brt) + expectedType = brt.ElementType; + else + expectedType = SpecialType.UnknownType; + value = stobj.Value; + return true; + default: + return false; + } + } + + internal override void CheckInvariant(ILPhase phase) + { + base.CheckInvariant(phase); + var patternVariables = new HashSet(); + var conversionVariables = new HashSet(); + + foreach (StLoc init in this.Init) { + Debug.Assert(init.Variable.IsSingleDefinition && init.Variable.LoadCount == 1); + Debug.Assert(init.Variable.LoadInstructions[0].IsDescendantOf(assignments)); + } + + ValidatePattern(pattern); + + foreach (var inst in this.conversions.Instructions) { + if (!IsConversionStLoc(inst, out var variable, out var inputVariable)) + Debug.Fail("inst is not a conversion stloc!"); + Debug.Assert(variable.IsSingleDefinition && variable.LoadCount == 1); + Debug.Assert(variable.LoadInstructions[0].IsDescendantOf(assignments)); + Debug.Assert(patternVariables.Contains(inputVariable)); + conversionVariables.Add(variable); + } + Debug.Assert(this.conversions.FinalInstruction is Nop); + + foreach (var inst in assignments.Instructions) { + if (!(IsAssignment(inst, typeSystem: null, out _, out var value) && value.MatchLdLoc(out var inputVariable))) + throw new InvalidOperationException("inst is not an assignment!"); + Debug.Assert(patternVariables.Contains(inputVariable) || conversionVariables.Contains(inputVariable)); + } + Debug.Assert(this.assignments.FinalInstruction is Nop); + + void ValidatePattern(MatchInstruction inst) + { + Debug.Assert(inst.IsDeconstructCall || inst.IsDeconstructTuple); + Debug.Assert(!inst.CheckNotNull && !inst.CheckType); + Debug.Assert(!inst.HasDesignator); + foreach (var subPattern in inst.SubPatterns.Cast()) { + if (subPattern.IsVar) { + Debug.Assert(subPattern.Variable.IsSingleDefinition && subPattern.Variable.LoadCount <= 1); + if (subPattern.Variable.LoadCount == 1) + Debug.Assert(subPattern.Variable.LoadInstructions[0].IsDescendantOf(this)); + patternVariables.Add(subPattern.Variable); + } else { + ValidatePattern(subPattern); + } + } + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs new file mode 100644 index 000000000..998a0e37c --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2020 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. + +using System.Diagnostics; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL +{ + partial class DeconstructResultInstruction + { + public int Index { get; } + + public override StackType ResultType { get; } + + public DeconstructResultInstruction(int index, StackType resultType, ILInstruction argument) + : base(OpCode.DeconstructResultInstruction, argument) + { + Debug.Assert(index >= 0); + Index = index; + ResultType = resultType; + } + + public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + output.Write(Index.ToString()); + output.Write('('); + this.Argument.WriteTo(output, options); + output.Write(')'); + } + + MatchInstruction FindMatch() + { + for (ILInstruction inst = this; inst != null; inst = inst.Parent) { + if (inst.Parent is MatchInstruction match && inst != match.TestedOperand) + return match; + } + return null; + } + + void AdditionalInvariants() + { + var matchInst = FindMatch(); + Debug.Assert(matchInst != null && (matchInst.IsDeconstructCall || matchInst.IsDeconstructTuple)); + Debug.Assert(Argument.MatchLdLoc(matchInst.Variable)); + var outParamType = matchInst.GetDeconstructResultType(this.Index); + Debug.Assert(outParamType.GetStackType() == ResultType); + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs index 1bc534954..01a37daa2 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs @@ -98,6 +98,86 @@ namespace ICSharpCode.Decompiler.IL } return false; } + + public ILInstruction GetCommonParent(ILInstruction other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + ILInstruction a = this; + ILInstruction b = other; + + int levelA = a.CountAncestors(); + int levelB = b.CountAncestors(); + + while (levelA > levelB) { + a = a.Parent; + levelA--; + } + + while (levelB > levelA) { + b = b.Parent; + levelB--; + } + + while (a != b) { + a = a.Parent; + b = b.Parent; + } + + return a; + } + + /// + /// Returns whether this appears before other in a post-order walk of the whole tree. + /// + public bool IsBefore(ILInstruction other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + ILInstruction a = this; + ILInstruction b = other; + + int levelA = a.CountAncestors(); + int levelB = b.CountAncestors(); + + int originalLevelA = levelA; + int originalLevelB = levelB; + + while (levelA > levelB) { + a = a.Parent; + levelA--; + } + + while (levelB > levelA) { + b = b.Parent; + levelB--; + } + + if (a == b) { + // a or b is a descendant of the other, + // whichever node has the higher level comes first in post-order walk. + return originalLevelA > originalLevelB; + } + + while (a.Parent != b.Parent) { + a = a.Parent; + b = b.Parent; + } + + // now a and b have the same parent or are both root nodes + return a.ChildIndex < b.ChildIndex; + } + + private int CountAncestors() + { + int level = 0; + for (ILInstruction ancestor = this; ancestor != null; ancestor = ancestor.Parent) { + level++; + } + return level; + } /// /// Gets the stack type of the value produced by this instruction. @@ -632,9 +712,10 @@ namespace ICSharpCode.Decompiler.IL /// Reference to the field holding the child /// New child /// Index of the field in the Children collection - protected internal void SetChildInstruction(ref ILInstruction childPointer, ILInstruction newValue, int index) + protected internal void SetChildInstruction(ref T childPointer, T newValue, int index) + where T : ILInstruction { - ILInstruction oldValue = childPointer; + T oldValue = childPointer; Debug.Assert(oldValue == GetChild(index)); if (oldValue == newValue && newValue?.parent == this && newValue.ChildIndex == index) return; diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs new file mode 100644 index 000000000..e29784ebe --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -0,0 +1,267 @@ +// Copyright (c) 2020 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. + +using System; +using System.Diagnostics; +using System.Linq; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL +{ + partial class MatchInstruction : ILInstruction + { + /* Pseudo-Code for interpreting a MatchInstruction: + bool Eval() + { + var value = this.TestedOperand.Eval(); + if (this.CheckNotNull && value == null) + return false; + if (this.CheckType && !(value is this.Variable.Type)) + return false; + if (this.IsDeconstructCall) { + deconstructResult = new[numArgs]; + EvalCall(this.Method, value, out deconstructResult[0], .., out deconstructResult[numArgs-1]); + // any occurrences of 'deconstruct.result' in the subPatterns will refer + // to the values provided by evaluating the call. + } + Variable.Value = value; + foreach (var subPattern in this.SubPatterns) { + if (!subPattern.Eval()) + return false; + } + return true; + } + */ + /* Examples of MatchInstructions: + expr is var x: + match(x = expr) + + expr is {} x: + match.notnull(x = expr) + + expr is T x: + match.type[T](x = expr) + + expr is C { A: var x } z: + match.type[C](z = expr) { + match(x = z.A) + } + + expr is C { A: var x, B: 42, C: { A: 4 } } z: + match.type[C](z = expr) { + match(x = z.A), + comp (z.B == 42), + match.notnull(temp2 = z.C) { + comp (temp2.A == 4) + } + } + + expr is C(var x, var y, <4): + match.type[C].deconstruct[C.Deconstruct](tmp1 = expr) { + match(x = deconstruct.result1(tmp1)), + match(y = deconstruct.result2(tmp1)), + comp(deconstruct.result3(tmp1) < 4), + } + + expr is C(1, D(2, 3)): + match.type[C].deconstruct(c = expr) { + comp(deconstruct.result1(c) == 1), + match.type[D].deconstruct(d = deconstruct.result2(c)) { + comp(deconstruct.result1(d) == 2), + comp(deconstruct.result2(d) == 2), + } + } + */ + + public bool IsVar => !CheckType && !CheckNotNull && !IsDeconstructCall && !IsDeconstructTuple && SubPatterns.Count == 0; + + public bool HasDesignator => Variable.LoadCount + Variable.AddressCount > SubPatterns.Count; + + public int NumPositionalPatterns { + get { + if (IsDeconstructCall) + return method.Parameters.Count - (method.IsStatic ? 1 : 0); + else if (IsDeconstructTuple) + return TupleType.GetTupleElementTypes(variable.Type).Length; + else + return 0; + } + } + + public MatchInstruction(ILVariable variable, ILInstruction testedOperand) + : this(variable, method: null, testedOperand) + { + } + + /// + /// Checks whether the input instruction can represent a pattern matching operation. + /// + /// Any pattern matching instruction will first evaluate the `testedOperand` (a descendant of `inst`), + /// and then match the value of that operand against the pattern encoded in the instruction. + /// The matching may have side-effects on the newly-initialized pattern variables + /// (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, out ILInstruction testedOperand) + { + switch (inst) { + case MatchInstruction m: + 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); + default: + testedOperand = null; + return false; + } + } + + private static bool IsConstant(ILInstruction inst) + { + return inst.OpCode switch + { + OpCode.LdcDecimal => true, + OpCode.LdcF4 => true, + OpCode.LdcF8 => true, + OpCode.LdcI4 => true, + OpCode.LdcI8 => true, + OpCode.LdNull => true, + _ => false + }; + } + + internal IType GetDeconstructResultType(int index) + { + if (this.IsDeconstructCall) { + int firstOutParam = (method.IsStatic ? 1 : 0); + var outParamType = this.Method.Parameters[firstOutParam + index].Type; + if (!(outParamType is ByReferenceType brt)) + throw new InvalidOperationException("deconstruct out param must be by reference"); + return brt.ElementType; + } + if (this.IsDeconstructTuple) { + var elementTypes = TupleType.GetTupleElementTypes(this.variable.Type); + return elementTypes[index]; + } + throw new InvalidOperationException("GetDeconstructResultType requires a deconstruct pattern"); + } + + void AdditionalInvariants() + { + Debug.Assert(variable.Kind == VariableKind.PatternLocal); + if (this.IsDeconstructCall) { + Debug.Assert(IsDeconstructMethod(method)); + } else { + Debug.Assert(method == null); + } + if (this.IsDeconstructTuple) { + Debug.Assert(variable.Type.Kind == TypeKind.Tuple); + } + Debug.Assert(SubPatterns.Count >= NumPositionalPatterns); + foreach (var subPattern in SubPatterns) { + if (!IsPatternMatch(subPattern, out ILInstruction operand)) + Debug.Fail("Sub-Pattern must be a valid pattern"); + // the first child is TestedOperand + int subPatternIndex = subPattern.ChildIndex - 1; + if (subPatternIndex < NumPositionalPatterns) { + // positional pattern + Debug.Assert(operand is DeconstructResultInstruction result && result.Index == subPatternIndex); + } else if (operand.MatchLdFld(out var target, out _)) { + Debug.Assert(target.MatchLdLoc(variable)); + } else if (operand is CallInstruction call) { + Debug.Assert(call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter); + Debug.Assert(call.Arguments[0].MatchLdLoc(variable)); + } else { + Debug.Fail("Tested operand of sub-pattern is invalid."); + } + } + } + + internal static bool IsDeconstructMethod(IMethod method) + { + if (method.Name != "Deconstruct") + return false; + if (method.ReturnType.Kind != TypeKind.Void) + return false; + int firstOutParam = (method.IsStatic ? 1 : 0); + if (method.IsStatic) { + if (!method.IsExtensionMethod) + return false; + // TODO : check whether all type arguments can be inferred from the first argument + } else { + if (method.TypeParameters.Count != 0) + return false; + } + + // TODO : check whether the method is ambigious + + if (method.Parameters.Count < firstOutParam) + return false; + + for (int i = firstOutParam; i < method.Parameters.Count; i++) { + if (!method.Parameters[i].IsOut) + return false; + } + + return true; + } + + public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + { + WriteILRange(output, options); + output.Write(OpCode); + if (CheckNotNull) { + output.Write(".notnull"); + } + if (CheckType) { + output.Write(".type["); + variable.Type.WriteTo(output); + output.Write(']'); + } + if (IsDeconstructCall) { + output.Write(".deconstruct["); + method.WriteTo(output); + output.Write(']'); + } + if (IsDeconstructTuple) { + output.Write(".tuple"); + } + output.Write(' '); + output.Write('('); + Variable.WriteTo(output); + output.Write(" = "); + TestedOperand.WriteTo(output, options); + output.Write(')'); + if (SubPatterns.Count > 0) { + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var pattern in SubPatterns) { + pattern.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.Write('}'); + output.MarkFoldEnd(); + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs new file mode 100644 index 000000000..409d3a907 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs @@ -0,0 +1,435 @@ +// Copyright (c) 2020 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. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Resources; + +using ICSharpCode.Decompiler.CSharp.Resolver; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + /// + /// + /// + class DeconstructionTransform : IStatementTransform + { + StatementTransformContext context; + readonly Dictionary deconstructionResultsLookup = new Dictionary(); + ILVariable[] deconstructionResults; + ILVariable tupleVariable; + TupleType tupleType; + + /* + stloc tuple(call MakeIntIntTuple(ldloc this)) + ---- + stloc myInt(call op_Implicit(ldfld Item2(ldloca tuple))) + stloc a(ldfld Item1(ldloca tuple)) + stloc b(ldloc myInt) + ==> + deconstruct { + init: + + deconstruct: + match.deconstruct(temp = ldloca tuple) { + match(result0 = deconstruct.result 0(temp)), + match(result1 = deconstruct.result 1(temp)) + } + conversions: { + stloc conv2(call op_Implicit(ldloc result1)) + } + assignments: { + stloc a(ldloc result0) + stloc b(ldloc conv2) + } + } + * */ + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) + { + if (!context.Settings.Deconstruction) + return; + + try { + this.context = context; + Reset(); + + if (TransformDeconstruction(block, pos)) + return; + if (InlineDeconstructionInitializer(block, pos)) + return; + } finally { + this.context = null; + Reset(); + } + } + + private void Reset() + { + this.deconstructionResultsLookup.Clear(); + this.tupleVariable = null; + this.tupleType = null; + this.deconstructionResults = null; + } + + struct ConversionInfo + { + public IType inputType; + public Conv conv; + } + + /// + /// Get index of deconstruction result or tuple element + /// Returns -1 on failure. + /// + int FindIndex(ILInstruction inst, out Action delayedActions) + { + delayedActions = null; + if (inst.MatchLdLoc(out var v)) { + if (!deconstructionResultsLookup.TryGetValue(v, out int index)) + return -1; + return index; + } + if (inst.MatchLdFld(out _, out _)) { + if (!TupleTransform.MatchTupleFieldAccess((LdFlda)((LdObj)inst).Target, out var tupleType, out var target, out int index)) + return -1; + // Item fields are one-based, we use zero-based indexing. + index--; + // normalize tuple type + tupleType = TupleType.FromUnderlyingType(context.TypeSystem, tupleType); + if (!target.MatchLdLoca(out v)) + return -1; + if (this.tupleVariable == null) { + this.tupleVariable = v; + this.tupleType = (TupleType)tupleType; + this.deconstructionResults = new ILVariable[this.tupleType.Cardinality]; + } + if (this.tupleType.Cardinality < 2) + return -1; + if (v != tupleVariable || !this.tupleType.Equals(tupleType)) + return -1; + if (this.deconstructionResults[index] == null) { + var freshVar = new ILVariable(VariableKind.StackSlot, this.tupleType.ElementTypes[index]) { Name = "E_" + index }; + delayedActions += _ => context.Function.Variables.Add(freshVar); + this.deconstructionResults[index] = freshVar; + } + delayedActions += _ => { + inst.ReplaceWith(new LdLoc(this.deconstructionResults[index])); + }; + return index; + } + return -1; + } + + /// + /// stloc v(value) + /// expr(..., deconstruct { ... }, ...) + /// => + /// expr(..., deconstruct { init: stloc v(value) ... }, ...) + /// + bool InlineDeconstructionInitializer(Block block, int pos) + { + if (!block.Instructions[pos].MatchStLoc(out var v, out var value)) + return false; + if (!(v.IsSingleDefinition && v.LoadCount == 1)) + return false; + if (pos + 1 >= block.Instructions.Count) + return false; + var result = ILInlining.FindLoadInNext(block.Instructions[pos + 1], v, value, InliningOptions.FindDeconstruction); + if (result.Type != ILInlining.FindResultType.Deconstruction) + return false; + var deconstruction = (DeconstructInstruction)result.LoadInst; + LdLoc loadInst = v.LoadInstructions[0]; + if (!loadInst.IsDescendantOf(deconstruction.Assignments)) + return false; + if (loadInst.SlotInfo == StObj.TargetSlot) { + if (value.OpCode == OpCode.LdFlda || value.OpCode == OpCode.LdElema) + return false; + } + if (deconstruction.Init.Count > 0) { + var a = deconstruction.Init[0].Variable.LoadInstructions.Single(); + var b = v.LoadInstructions.Single(); + if (!b.IsBefore(a)) + return false; + } + context.Step("InlineDeconstructionInitializer", block.Instructions[pos]); + deconstruction.Init.Insert(0, (StLoc)block.Instructions[pos]); + block.Instructions.RemoveAt(pos); + v.Kind = VariableKind.DeconstructionInitTemporary; + return true; + } + + bool TransformDeconstruction(Block block, int pos) + { + int startPos = pos; + Action delayedActions = null; + if (MatchDeconstruction(block.Instructions[pos], out IMethod deconstructMethod, + out ILInstruction rootTestedOperand)) + { + pos++; + } + if (!MatchConversions(block, ref pos, out var conversions, out var conversionStLocs, ref delayedActions)) + return false; + + if (!MatchAssignments(block, ref pos, conversions, conversionStLocs, ref delayedActions)) + return false; + // first tuple element may not be discarded, + // otherwise we would run this transform on a suffix of the actual pattern. + if (deconstructionResults[0] == null) + return false; + context.Step("Deconstruction", block.Instructions[startPos]); + DeconstructInstruction replacement = new DeconstructInstruction(); + IType deconstructedType; + if (deconstructMethod == null) { + deconstructedType = this.tupleType; + rootTestedOperand = new LdLoc(this.tupleVariable); + } else { + if (deconstructMethod.IsStatic) { + deconstructedType = deconstructMethod.Parameters[0].Type; + } else { + deconstructedType = deconstructMethod.DeclaringType; + } + } + var rootTempVariable = context.Function.RegisterVariable(VariableKind.PatternLocal, deconstructedType); + replacement.Pattern = new MatchInstruction(rootTempVariable, deconstructMethod, rootTestedOperand) { + IsDeconstructCall = deconstructMethod != null, + IsDeconstructTuple = this.tupleType != null + }; + int index = 0; + foreach (ILVariable v in deconstructionResults) { + var result = v; + if (result == null) { + var freshVar = new ILVariable(VariableKind.PatternLocal, this.tupleType.ElementTypes[index]) { Name = "E_" + index }; + context.Function.Variables.Add(freshVar); + result = freshVar; + } else { + result.Kind = VariableKind.PatternLocal; + } + replacement.Pattern.SubPatterns.Add( + new MatchInstruction( + result, + new DeconstructResultInstruction(index, result.StackType, new LdLoc(rootTempVariable)) + ) + ); + index++; + } + replacement.Conversions = new Block(BlockKind.DeconstructionConversions); + foreach (var convInst in conversionStLocs) { + replacement.Conversions.Instructions.Add(convInst); + } + replacement.Assignments = new Block(BlockKind.DeconstructionAssignments); + delayedActions?.Invoke(replacement); + block.Instructions[startPos] = replacement; + block.Instructions.RemoveRange(startPos + 1, pos - startPos - 1); + return true; + } + + bool MatchDeconstruction(ILInstruction inst, out IMethod deconstructMethod, + out ILInstruction testedOperand) + { + testedOperand = null; + deconstructMethod = null; + deconstructionResults = null; + if (!(inst is CallInstruction call)) + return false; + if (!MatchInstruction.IsDeconstructMethod(call.Method)) + return false; + if (call.Method.IsStatic == call is CallVirt) + return false; + if (call.Arguments.Count < 3) + return false; + deconstructionResults = new ILVariable[call.Arguments.Count - 1]; + for (int i = 0; i < deconstructionResults.Length; i++) { + if (!call.Arguments[i + 1].MatchLdLoca(out var v)) + return false; + // TODO v.LoadCount may be 2 if the deconstruction is assigned to a tuple variable + // or 0? because of discards + if (!(v.StoreCount == 0 && v.AddressCount == 1 && v.LoadCount <= 1)) + return false; + deconstructionResultsLookup.Add(v, i); + deconstructionResults[i] = v; + } + testedOperand = call.Arguments[0]; + deconstructMethod = call.Method; + return true; + } + + bool MatchConversions(Block block, ref int pos, + out Dictionary conversions, + out List conversionStLocs, + ref Action delayedActions) + { + conversions = new Dictionary(); + conversionStLocs = new List(); + int previousIndex = -1; + while (MatchConversion( + block.Instructions.ElementAtOrDefault(pos), out var inputInstruction, + out var outputVariable, out var info)) { + int index = FindIndex(inputInstruction, out var tupleAccessAdjustment); + if (index <= previousIndex) + return false; + if (!(outputVariable.IsSingleDefinition && outputVariable.LoadCount == 1)) + return false; + delayedActions += tupleAccessAdjustment; + deconstructionResultsLookup.Add(outputVariable, index); + conversions.Add(outputVariable, info); + conversionStLocs.Add((StLoc)block.Instructions[pos]); + pos++; + previousIndex = index; + } + return true; + } + + bool MatchConversion(ILInstruction inst, out ILInstruction inputInstruction, + out ILVariable outputVariable, out ConversionInfo info) + { + info = default; + inputInstruction = null; + if (!inst.MatchStLoc(out outputVariable, out var value)) + return false; + if (!(value is Conv conv)) + return false; + info = new ConversionInfo { + inputType = conv.Argument.InferType(context.TypeSystem), + conv = conv + }; + inputInstruction = conv.Argument; + return true; + } + + bool MatchAssignments(Block block, ref int pos, + Dictionary conversions, + List conversionStLocs, + ref Action delayedActions) + { + int previousIndex = -1; + int conversionStLocIndex = 0; + int startPos = pos; + while (MatchAssignment(block.Instructions.ElementAtOrDefault(pos), out var targetType, out var valueInst, out var addAssignment)) { + int index = FindIndex(valueInst, out var tupleAccessAdjustment); + if (index <= previousIndex) + return false; + AddMissingAssignmentsForConversions(index, ref delayedActions); + if (!(valueInst.MatchLdLoc(out var resultVariable) + && conversions.TryGetValue(resultVariable, out var conversionInfo))) + { + conversionInfo = new ConversionInfo { + inputType = valueInst.InferType(context.TypeSystem) + }; + } + if (block.Instructions[pos].MatchStLoc(out var assignmentTarget, out _) + && assignmentTarget.Kind == VariableKind.StackSlot + && assignmentTarget.IsSingleDefinition + && conversionInfo.conv == null) { + delayedActions += _ => { + assignmentTarget.Type = conversionInfo.inputType; + }; + } else { + if (!IsCompatibleImplicitConversion(targetType, conversionInfo)) + return false; + } + delayedActions += addAssignment; + delayedActions += tupleAccessAdjustment; + pos++; + previousIndex = index; + } + AddMissingAssignmentsForConversions(int.MaxValue, ref delayedActions); + return startPos != pos; + + void AddMissingAssignmentsForConversions(int index, ref Action delayedActions) + { + while (conversionStLocIndex < conversionStLocs.Count) { + var stLoc = conversionStLocs[conversionStLocIndex]; + int conversionResultIndex = deconstructionResultsLookup[stLoc.Variable]; + + if (conversionResultIndex >= index) + break; + if (conversionResultIndex > previousIndex) { + delayedActions += (DeconstructInstruction deconstructInst) => { + var freshVar = context.Function.RegisterVariable(VariableKind.StackSlot, stLoc.Variable.Type); + deconstructInst.Assignments.Instructions.Add(new StLoc(stLoc.Variable, new LdLoc(freshVar))); + stLoc.Variable = freshVar; + }; + } + conversionStLocIndex++; + } + } + } + + bool MatchAssignment(ILInstruction inst, out IType targetType, out ILInstruction valueInst, out Action addAssignment) + { + targetType = null; + valueInst = null; + addAssignment = null; + if (inst == null) + return false; + if (inst.MatchStLoc(out var v, out var value) + && value is Block block && block.MatchInlineAssignBlock(out var call, out valueInst)) { + if (!DeconstructInstruction.IsAssignment(call, context.TypeSystem, out targetType, out _)) + return false; + if (!(v.IsSingleDefinition && v.LoadCount == 0)) + return false; + var valueInstCopy = valueInst; + addAssignment = (DeconstructInstruction deconstructInst) => { + call.Arguments[call.Arguments.Count - 1] = valueInstCopy; + deconstructInst.Assignments.Instructions.Add(call); + }; + return true; + } else if (DeconstructInstruction.IsAssignment(inst, context.TypeSystem, out targetType, out valueInst)) { + // OK - use the assignment as is + addAssignment = (DeconstructInstruction deconstructInst) => { + deconstructInst.Assignments.Instructions.Add(inst); + }; + return true; + } else { + return false; + } + } + + bool IsCompatibleImplicitConversion(IType targetType, ConversionInfo conversionInfo) + { + var c = CSharpConversions.Get(context.TypeSystem) + .ImplicitConversion(conversionInfo.inputType, targetType); + if (!c.IsValid) + return false; + var inputType = conversionInfo.inputType; + var conv = conversionInfo.conv; + if (c.IsIdentityConversion || c.IsReferenceConversion) { + return conv == null || conv.Kind == ConversionKind.Nop; + } + if (c.IsNumericConversion) { + switch (conv.Kind) { + case ConversionKind.IntToFloat: + return inputType.GetSign() == conv.InputSign; + case ConversionKind.FloatPrecisionChange: + return true; + case ConversionKind.SignExtend: + return inputType.GetSign() == Sign.Signed; + case ConversionKind.ZeroExtend: + return inputType.GetSign() == Sign.Unsigned; + default: + return false; + } + } + return false; + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 5316249e2..82225949c 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -31,6 +31,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms None = 0, Aggressive = 1, IntroduceNamedArguments = 2, + FindDeconstruction = 4, } /// @@ -503,6 +504,8 @@ 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; } // decide based on the top-level target instruction into which we are inlining: switch (next.OpCode) { @@ -543,8 +546,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// Found a load in call, but re-ordering not possible with regards to the /// other call arguments. /// Inlining is not possible, but we might convert the call to named arguments. + /// Only used with . /// NamedArgument, + /// + /// Found a deconstruction. + /// Only used with . + /// + Deconstruction, } internal readonly struct FindResult @@ -575,6 +584,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms Debug.Assert(callArg.Parent is CallInstruction); return new FindResult(FindResultType.NamedArgument, loadInst, callArg); } + + public static FindResult Deconstruction(DeconstructInstruction deconstruction) + { + return new FindResult(FindResultType.Deconstruction, deconstruction, null); + } } /// @@ -611,6 +625,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms default: return FindResult.Stop; } + } else if (options.HasFlag(InliningOptions.FindDeconstruction) && expr is DeconstructInstruction di) { + return FindResult.Deconstruction(di); } foreach (var child in expr.Children) { if (!expr.CanInlineIntoSlot(child.ChildIndex, expressionBeingMoved)) diff --git a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs index 82730d64f..ce2f7c49b 100644 --- a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs +++ b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs @@ -164,7 +164,7 @@ namespace ICSharpCode.Decompiler if (node is Identifier && node.Parent != null) node = node.Parent; - if (node is ParameterDeclaration || node is VariableInitializer || node is CatchClause || node is ForeachStatement) { + if (node is ParameterDeclaration || node is VariableInitializer || node is CatchClause || node is VariableDesignation) { var variable = node.Annotation()?.Variable; if (variable != null) return variable; diff --git a/ICSharpCode.Decompiler/TypeSystem/TupleType.cs b/ICSharpCode.Decompiler/TypeSystem/TupleType.cs index 7833b84c5..906fa413a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TupleType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TupleType.cs @@ -44,6 +44,11 @@ namespace ICSharpCode.Decompiler.TypeSystem /// public ImmutableArray ElementTypes { get; } + /// + /// Gets the cardinality of the tuple. + /// + public int Cardinality => ElementTypes.Length; + /// /// Gets the names of the tuple elements. /// diff --git a/ILSpy.BamlDecompiler/Properties/launchSettings.json b/ILSpy.BamlDecompiler/Properties/launchSettings.json new file mode 100644 index 000000000..253f16b70 --- /dev/null +++ b/ILSpy.BamlDecompiler/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "ILSpy.BamlDecompiler": { + "commandName": "Executable", + "executablePath": "C:\\Users\\sie_p\\Projects\\ILSpy\\ILSpy\\bin\\Debug\\net472\\ILSpy.exe", + "commandLineArgs": "/separate" + } + } +} \ No newline at end of file diff --git a/ILSpy.ReadyToRun/Properties/Resources.Designer.cs b/ILSpy.ReadyToRun/Properties/Resources.Designer.cs index 56cf7ac1f..f28df710f 100644 --- a/ILSpy.ReadyToRun/Properties/Resources.Designer.cs +++ b/ILSpy.ReadyToRun/Properties/Resources.Designer.cs @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// 此代码由工具生成。 -// 运行时版本:4.0.30319.42000 +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // -// 对此文件的更改可能会导致不正确的行为,并且如果 -// 重新生成代码,这些更改将会丢失。 +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. // //------------------------------------------------------------------------------ @@ -13,12 +13,12 @@ namespace ILSpy.ReadyToRun.Properties { /// - /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// A strongly-typed resource class, for looking up localized strings, etc. /// - // 此类是由 StronglyTypedResourceBuilder - // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 - // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen - // (以 /str 作为命令选项),或重新生成 VS 项目。 + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -33,7 +33,7 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 返回此类使用的缓存的 ResourceManager 实例。 + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 重写当前线程的 CurrentUICulture 属性 - /// 重写当前线程的 CurrentUICulture 属性。 + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Globalization.CultureInfo Culture { @@ -61,7 +61,7 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 查找类似 Disassembly Format 的本地化字符串。 + /// Looks up a localized string similar to Disassembly Format. /// public static string DisassemblyFormat { get { @@ -70,7 +70,7 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 查找类似 ReadyToRun 的本地化字符串。 + /// Looks up a localized string similar to ReadyToRun. /// public static string ReadyToRun { get { @@ -79,7 +79,7 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 查找类似 Show Debug Info 的本地化字符串。 + /// Looks up a localized string similar to Show Debug Info. /// public static string ShowDebugInfo { get { @@ -88,7 +88,7 @@ namespace ILSpy.ReadyToRun.Properties { } /// - /// 查找类似 Show Unwind Info 的本地化字符串。 + /// Looks up a localized string similar to Show Unwind Info. /// public static string ShowUnwindInfo { get { diff --git a/ILSpy.ReadyToRun/Properties/launchSettings.json b/ILSpy.ReadyToRun/Properties/launchSettings.json new file mode 100644 index 000000000..5df6a1d3d --- /dev/null +++ b/ILSpy.ReadyToRun/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "ILSpy.ReadyToRun": { + "commandName": "Executable", + "executablePath": "C:\\Users\\sie_p\\Projects\\ILSpy\\ILSpy\\bin\\Debug\\net472\\ILSpy.exe", + "commandLineArgs": "/separate /language:ReadyToRun" + } + } +} \ No newline at end of file diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 197bd9ed7..56297314e 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -845,11 +845,19 @@ - + + + + + + + + powershell -NoProfile -ExecutionPolicy Bypass -File BuildTools/sort-resx.ps1 + diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index dc324a9e4..72d584ce8 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -810,6 +810,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Detect deconstruction assignments. + /// + public static string DecompilerSettings_Deconstruction { + get { + return ResourceManager.GetString("DecompilerSettings.Deconstruction", resourceCulture); + } + } + /// /// Looks up a localized string similar to Detect awaited using and foreach statements. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index e61f61bb9..735d599d4 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -300,6 +300,9 @@ Are you sure you want to continue? Decompile use of the 'dynamic' type + + Detect deconstruction assignments + Detect awaited using and foreach statements diff --git a/ILSpy/TreeNodes/ResourceNodes/JsonResourceNode.cs b/ILSpy/TreeNodes/ResourceNodes/JsonResourceNode.cs new file mode 100644 index 000000000..2009d3785 --- /dev/null +++ b/ILSpy/TreeNodes/ResourceNodes/JsonResourceNode.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// 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 System; +using System.ComponentModel.Composition; +using System.IO; +using System.Threading.Tasks; + +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpy.ViewModels; + +namespace ICSharpCode.ILSpy.TreeNodes +{ + [Export(typeof(IResourceNodeFactory))] + sealed class JsonResourceNodeFactory : IResourceNodeFactory + { + private readonly static string[] jsonFileExtensions = { ".json" }; + + public ILSpyTreeNode CreateNode(Resource resource) + { + Stream stream = resource.TryOpenStream(); + if (stream == null) + return null; + return CreateNode(resource.Name, stream); + } + + public ILSpyTreeNode CreateNode(string key, object data) + { + if (!(data is Stream)) + return null; + foreach (string fileExt in jsonFileExtensions) + { + if (key.EndsWith(fileExt, StringComparison.OrdinalIgnoreCase)) + return new JsonResourceEntryNode(key, (Stream)data); + } + return null; + } + } + + sealed class JsonResourceEntryNode : ResourceEntryNode + { + string json; + + public JsonResourceEntryNode(string key, Stream data) + : base(key, data) + { + } + + // TODO : add Json Icon + public override object Icon => Images.Resource; + + public override bool View(TabPageModel tabPage) + { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + IHighlightingDefinition highlighting = null; + + tabPage.ShowTextView(textView => textView.RunWithCancellation( + token => Task.Factory.StartNew( + () => { + try { + // cache read XAML because stream will be closed after first read + if (json == null) { + using (var reader = new StreamReader(Data)) { + json = reader.ReadToEnd(); + } + } + output.Write(json); + highlighting = HighlightingManager.Instance.GetDefinitionByExtension(".json"); + } + catch (Exception ex) { + output.Write(ex.ToString()); + } + return output; + }, token) + ).Then(t => textView.ShowNode(t, this, highlighting)) + .HandleExceptions()); + tabPage.SupportsLanguageSwitching = false; + return true; + } + } +} diff --git a/ILSpy/TreeNodes/ResourceNodes/TextResourceNode.cs b/ILSpy/TreeNodes/ResourceNodes/TextResourceNode.cs new file mode 100644 index 000000000..035b944e8 --- /dev/null +++ b/ILSpy/TreeNodes/ResourceNodes/TextResourceNode.cs @@ -0,0 +1,98 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// 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 System; +using System.ComponentModel.Composition; +using System.IO; +using System.Threading.Tasks; + +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpy.TextView; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpy.ViewModels; + +namespace ICSharpCode.ILSpy.TreeNodes +{ + [Export(typeof(IResourceNodeFactory))] + sealed class TextResourceNodeFactory : IResourceNodeFactory + { + private readonly static string[] txtFileExtensions = { ".txt", ".md" }; + + public ILSpyTreeNode CreateNode(Resource resource) + { + Stream stream = resource.TryOpenStream(); + if (stream == null) + return null; + return CreateNode(resource.Name, stream); + } + + public ILSpyTreeNode CreateNode(string key, object data) + { + if (!(data is Stream)) + return null; + foreach (string fileExt in txtFileExtensions) + { + if (key.EndsWith(fileExt, StringComparison.OrdinalIgnoreCase)) + return new TextResourceEntryNode(key, (Stream)data); + } + return null; + } + } + + sealed class TextResourceEntryNode : ResourceEntryNode + { + string text; + + public TextResourceEntryNode(string key, Stream data) + : base(key, data) + { + } + + public override object Icon => Images.Resource; + + public override bool View(TabPageModel tabPage) + { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + IHighlightingDefinition highlighting = null; + + tabPage.ShowTextView(textView => textView.RunWithCancellation( + token => Task.Factory.StartNew( + () => { + try { + // cache read text because stream will be closed after first read + if (text == null) { + using (var reader = new StreamReader(Data)) { + text = reader.ReadToEnd(); + } + } + output.Write(text); + highlighting = null; + } + catch (Exception ex) { + output.Write(ex.ToString()); + } + return output; + }, token) + ).Then(t => textView.ShowNode(t, this, highlighting)) + .HandleExceptions()); + tabPage.SupportsLanguageSwitching = false; + return true; + } + } +} diff --git a/doc/ILAst Pattern Matching.md b/doc/ILAst Pattern Matching.md new file mode 100644 index 000000000..fab710123 --- /dev/null +++ b/doc/ILAst Pattern Matching.md @@ -0,0 +1,152 @@ +ILAst Pattern Matching +================= + +Some IL instructions are classified as "patterns". +```c# +abstract class PatternMatchILInstruction : IStoreInstruction { + public ILInstruction TestedOperand { get; } + public ILVariable Variable { get; } // variable that receives the match result; may also be a temporary +} + +public bool IsPattern(this ILInstruction inst, out ILInstruction testedOperand) => inst switch { + PatternMatchILInstruction pm => testedOperand = pm.TestedOperand; return true; + LogicNot logicNot => IsPattern(logicNot.Operand, out testedOperand), + Comp comp => testedOperand = comp.Left; return IsConstant(comp.Right); +} +``` +Every `match.*` instruction has the following properties: + * The `TestedOperand` specifies what gets matched against the pattern. + * The `Variable` stores the value of `TestedOperand` (after converting to the matched type, if appropriate). + * If this variable is also used outside the `match.*` node, it corresponds to the C# `single_variable_designation`. + * Otherwise it's a temporary used for pattern matching. + * I think in both cases it should have VariableKind.PatternVar + * The match instruction evaluates to `StackType.I4`: 0 if the pattern was matched, 1 otherwise. + +Some `match` instructions have a body with `List nestedPatterns`. Here every nested pattern must be a pattern according to `IsPattern()`, and the `testedOperand` of each must be a member of the `Variable` of the parent pattern. (members are: field, property, or deconstruction.result). +(exception: `match.and`/`match.or`, these instead require the `testedOperand` to be exactly the `Variable` of the parent pattern) + + +Examples +-------- +1) `expr is var x` + => + `match.var(x = expr)` + => + ``` + Block (VarPattern) { + stloc x(expr) // single eval expr + final: ldc.i4 1 // match always + } + ``` +2) `expr is T x` + => + `match.type T(x = expr) {}` + => + ``` + Block (TypePattern) { + stloc x(isinst T(expr)) + final: x != null + } + ``` +3) `expr is C { A: var x } z` + => + ``` + match.type C(z = expr) { + match.var(x = z.A) + } + ``` + => + ``` + Block (TypePattern) { + stloc z(isinst T(expr)) + final: (z != null) + && Block(VarPattern) { + stloc x(z.A) + final: ldc.i4 1 + } + } + ``` +4) `expr is C { A: var x, B: 42, C: { A: 4 } } z` + => + ``` + match.type C(z = expr) { + match.var (x = z.A), + comp (z.B == 42), + match.recursive (temp2 = z.C) { + comp (temp2.A == 4) + } + } + ``` + => + ``` + Block (TypePattern) { + stloc z(isinst C(expr)) + final: (z != null) + && Block(VarPattern) { + stloc x(z.A) + final: ldc.i4 1 + } + && comp (z.B == 42) + && Block(RecursivePattern) { + stloc temp2(z.C) + final: (temp2 != null) + && comp (temp2.A == 4) + } + } + ``` +5) `expr is C(var x, var y, <4) { ... }` + => + ``` + match.recursive.type.deconstruct(C tmp1 = expr) { + match.var(x = deconstruct.result0(tmp1)), + match.var(y = deconstruct.result1(tmp1)), + comp(deconstruct.result2(tmp1) < 4), + } + ``` + +6) `expr is C(1, D(2, 3))` + => + ``` + match.type.deconstruct(C c = expr) { + comp(deconstruct.result0(c) == 1), + match.type.deconstruct(D d = deconstruct.result1(c)) { + comp(deconstruct.result0(d) == 2), + comp(deconstruct.result1(d) == 2), + } + } + ``` + +7) `x is >= 0 and var y and <= 100` + ``` + match.and(tmp1 = x) { + comp(tmp1 >= 0), + match.var(y = tmp1), + comp(tmp1 <= 100) + } + ``` + +8) `x is not C _` + => + ``` + logic.not( + match.type(C tmp1 = x) {} + ) + ``` + +9) `expr is (var a, var b)` (when expr is object) + => + ``` + match.type.deconstruct(ITuple tmp = expr) { + match.var(a = deconstruct.result0(tmp)), + match.var(b = deconstruct.result1(tmp)), + } + ``` + +10) `expr is (var a, var b)` (when expr is ValueTuple) + => + ``` + match.recursive(tmp = expr) { + match.var(a = tmp.Item1), + match.var(b = tmp.Item2), + } + ``` \ No newline at end of file diff --git a/msbuild.binlog b/msbuild.binlog new file mode 100644 index 000000000..357791088 Binary files /dev/null and b/msbuild.binlog differ