diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs index 15c49cb9a8..ca599ab5c6 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs @@ -15,6 +15,15 @@ public static void Deconstruct(this KeyValuePair pai } } + static class TupleClassExtensions + { + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + } + } + class DeconstructionTests { public static void Main() @@ -153,6 +162,309 @@ public void Test() new NestedOuter { Value = 2 } }); NestedDeconstruction_DiscardedElement(new KeyValuePair("key", default(DiscardData))); + NestedDeconstruction_ClassInner(new ClassInnerOuter { Value = 7 }); + NestedDeconstruction_Depth3(new DeepOuter { Value = 3 }); + NestedDeconstruction_LhsSideEffects_DeconstructionOrder_Assignments(); + NestedDeconstruction_Conversions_AfterAllDeconstructCalls(); + NestedDeconstruction_TypedDeclaration_Conversions(new NestedOuter { Value = 5 }); + NestedDeconstruction_DiscardWithSideEffectTargets(); + NestedDeconstruction_HiddenDeconstructMethod(default(HidingOuter)); + NestedDeconstruction_TupleWithCustomElement((7, new NestedInner { Value = 3 })); + NestedDeconstruction_SystemTupleSource(Tuple.Create(8, new NestedInner { Value = 4 })); + NestedDeconstruction_CheckedConversions(new NestedOuter { Value = 9 }); + NestedDeconstruction_GenericConstraintSource(new ConstrainedSource { Value = 11 }); + NestedDeconstruction_InParameterSource(new NestedOuter { Value = 12 }); + NestedDeconstruction_ConditionalSource(c: true, new NestedOuter { Value = 13 }, new NestedOuter { Value = 14 }); + NestedDeconstruction_TupleOuterConversions((15, new NestedInner { Value = 6 })); + NestedDeconstruction_TypedConversions_UnrelatedCallAfter(new NestedOuter { Value = 16 }); + NestedDeconstruction_NullableConversions(new NestedOuter { Value = 17 }); + NestedDeconstruction_MyIntConversionOnNestedLeaves(new NestedOuter { Value = 18 }); + NestedDeconstruction_ForEachDictionary_Conversions(new Dictionary { + { "k1", new NestedInner { Value = 19 } } + }); + NestedTupleDeconstruction_Values((20, (21, 22))); + NestedTupleDeconstruction_Depth3((23, (24, (25, 26)))); + NestedTupleDeconstruction_Conversions((27, (28, 29))); + NestedTupleDeconstruction_ElementUsedOutside((30, (31, 32))); + NestedTupleDeconstruction_ForEach(new List<(int, (int, int))> { + (33, (34, 35)), + (36, (37, 38)) + }); + } + + public void NestedTupleDeconstruction_Values((int, (int, int)) t) + { + Console.WriteLine("NestedTupleDeconstruction_Values:"); + var (x, (a, b)) = t; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedTupleDeconstruction_Depth3((int, (int, (int, int))) t) + { + Console.WriteLine("NestedTupleDeconstruction_Depth3:"); + var (x, (a, (b, c))) = t; + Console.WriteLine(x + " " + a + " " + b + " " + c); + } + + public void NestedTupleDeconstruction_Conversions((int, (int, int)) t) + { + Console.WriteLine("NestedTupleDeconstruction_Conversions:"); + (long x, (long a, long b)) = t; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedTupleDeconstruction_ElementUsedOutside((int, (int, int)) t) + { + Console.WriteLine("NestedTupleDeconstruction_ElementUsedOutside:"); + var (x, inner) = t; + Console.WriteLine(x + " " + inner.Item1 + " " + inner.Item2); + } + + public void NestedTupleDeconstruction_ForEach(List<(int, (int, int))> list) + { + Console.WriteLine("NestedTupleDeconstruction_ForEach:"); + foreach (var (x, (a, b)) in list) + { + Console.WriteLine(x + " " + a + " " + b); + } + } + + public class ConstrainedSource + { + public int Value; + + public void Deconstruct(out int a, out NestedInner inner) + { + Console.WriteLine("ConstrainedSource.Deconstruct"); + a = Value; + inner = new NestedInner { Value = Value * 10 }; + } + } + + public void NestedDeconstruction_SystemTupleSource(Tuple tup) + { + Console.WriteLine("NestedDeconstruction_SystemTupleSource:"); + (long x, (long a, long b)) = tup; + int z = Side(); + Console.WriteLine(x + " " + a + " " + b + " " + z); + } + + public void NestedDeconstruction_CheckedConversions(NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_CheckedConversions:"); + checked + { + (long x, (long a, long b)) = o; + Console.WriteLine(x + " " + a + " " + b); + } + } + + public void NestedDeconstruction_GenericConstraintSource(T o) where T : ConstrainedSource + { + Console.WriteLine("NestedDeconstruction_GenericConstraintSource:"); + var (a, (c, d)) = o; + Console.WriteLine(a + " " + c + " " + d); + } + + public void NestedDeconstruction_InParameterSource(in NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_InParameterSource:"); + (long x, (long a, long b)) = o; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedDeconstruction_ConditionalSource(bool c, NestedOuter o1, NestedOuter o2) + { + Console.WriteLine("NestedDeconstruction_ConditionalSource:"); + (long x, (int a, int b)) = c ? o1 : o2; + int z = Side(); + Console.WriteLine(x + " " + a + " " + b + " " + z); + } + + public void NestedDeconstruction_TupleOuterConversions((int, NestedInner) tup) + { + Console.WriteLine("NestedDeconstruction_TupleOuterConversions:"); + (long x, (long a, long b)) = tup; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedDeconstruction_TypedConversions_UnrelatedCallAfter(NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_TypedConversions_UnrelatedCallAfter:"); + (long x, (long a, long b)) = o; + int z = Side(); + Console.WriteLine(x + " " + a + " " + b + " " + z); + } + + public void NestedDeconstruction_NullableConversions(NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_NullableConversions:"); + (long? x, (long? a, int? b)) = o; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedDeconstruction_MyIntConversionOnNestedLeaves(NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_MyIntConversionOnNestedLeaves:"); + (MyInt x, (MyInt a, long b)) = o; + Console.WriteLine(x + " " + a + " " + b); + } + + public void NestedDeconstruction_ForEachDictionary_Conversions(Dictionary d) + { + Console.WriteLine("NestedDeconstruction_ForEachDictionary_Conversions:"); + foreach ((string k, (long a, long b)) in d) + { + Console.WriteLine(k + " " + a + " " + b); + } + } + + // The evaluation order of a deconstruction-assignment is: (1) all side-effects of + // the left-hand-side targets, (2) all Deconstruct invocations, (3) conversions, + // (4) assignments. Get(i), the Deconstruct methods, MyInt's implicit conversions, + // and the property setters all print, so any phase reordering breaks the output diff. + public void NestedDeconstruction_LhsSideEffects_DeconstructionOrder_Assignments() + { + Console.WriteLine("NestedDeconstruction_LhsSideEffects_DeconstructionOrder_Assignments:"); + (Get(0).IntProperty, (Get(1).IntProperty, Get(2).IntProperty)) = new NestedOuter { Value = 11 }; + } + + public void NestedDeconstruction_Conversions_AfterAllDeconstructCalls() + { + Console.WriteLine("NestedDeconstruction_Conversions_AfterAllDeconstructCalls:"); + (Get(0).My, (Get(1).IntProperty, Get(2).My)) = new NestedOuter { Value = 21 }; + } + + public void NestedDeconstruction_TypedDeclaration_Conversions(NestedOuter o) + { + Console.WriteLine("NestedDeconstruction_TypedDeclaration_Conversions:"); + (MyInt x, (long a, MyInt b)) = o; + Console.WriteLine(x); + Console.WriteLine(a); + Console.WriteLine(b); + } + + public void NestedDeconstruction_DiscardWithSideEffectTargets() + { + Console.WriteLine("NestedDeconstruction_DiscardWithSideEffectTargets:"); + (Get(0).IntProperty, (_, Get(1).My)) = new NestedOuter { Value = 31 }; + } + + public class HidingBase + { + public int Value; + + public void Deconstruct(out string a, out double b) + { + Console.WriteLine("HidingBase.Deconstruct"); + a = "base" + Value; + b = 0.5; + } + } + + public class HidingDerived : HidingBase + { + public new void Deconstruct(out string a, out double b) + { + Console.WriteLine("HidingDerived.Deconstruct"); + a = "derived"; + b = 99.5; + } + } + + public struct HidingOuter + { + public void Deconstruct(out int x, out HidingDerived d) + { + Console.WriteLine("HidingOuter.Deconstruct"); + x = 1; + d = new HidingDerived { Value = 5 }; + } + } + + // The base-typed view forces the call to bind to HidingBase.Deconstruct; a nested + // designation cannot express that, because it rebinds on the element's static type, + // where the hiding method wins. + public void NestedDeconstruction_HiddenDeconstructMethod(HidingOuter o) + { + Console.WriteLine("NestedDeconstruction_HiddenDeconstructMethod:"); + var (_, d) = o; + HidingBase b = d; + var (a, c) = b; + Console.WriteLine(a); + Console.WriteLine(c); + } + + public int Side() + { + Console.WriteLine("Side()"); + return 5; + } + + // A tuple deconstruction whose element is custom-deconstructed, followed by an + // unrelated assignment: the tuple part must not be consumed into a pattern rooted + // in the element's Deconstruct call. + public void NestedDeconstruction_TupleWithCustomElement((int, NestedInner) tup) + { + Console.WriteLine("NestedDeconstruction_TupleWithCustomElement:"); + var (x, (_, _)) = tup; + int z = Side(); + Console.WriteLine(x * x + z); + } + + public class NestedClassInner + { + public int Value; + + public void Deconstruct(out int a, out int b) + { + Console.WriteLine("NestedClassInner.Deconstruct"); + a = Value + 1; + b = Value + 2; + } + } + + public struct ClassInnerOuter + { + public int Value; + + public void Deconstruct(out int x, out NestedClassInner inner) + { + Console.WriteLine("ClassInnerOuter.Deconstruct"); + x = Value; + inner = new NestedClassInner { Value = Value * 10 }; + } + } + + public void NestedDeconstruction_ClassInner(ClassInnerOuter o) + { + Console.WriteLine("NestedDeconstruction_ClassInner:"); + var (x, (a, b)) = o; + Console.WriteLine(x); + Console.WriteLine(a); + Console.WriteLine(b); + } + + public struct DeepOuter + { + public int Value; + + public void Deconstruct(out int x, out ClassInnerOuter mid) + { + Console.WriteLine("DeepOuter.Deconstruct"); + x = Value; + mid = new ClassInnerOuter { Value = Value * 100 }; + } + } + + public void NestedDeconstruction_Depth3(DeepOuter o) + { + Console.WriteLine("NestedDeconstruction_Depth3:"); + var (x, (y, (a, b))) = o; + Console.WriteLine(x); + Console.WriteLine(y); + Console.WriteLine(a); + Console.WriteLine(b); } public struct DiscardData diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs index 1e1802eb26..1fc53d916a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs @@ -22,6 +22,14 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { + public class DeconstructionBase + { + } + + public class DeconstructionDerived : DeconstructionBase + { + } + public static class DeconstructionExt { public static void Deconstruct(this KeyValuePair pair, out K key, out V value) @@ -29,6 +37,33 @@ public static void Deconstruct(this KeyValuePair pair, out K key, ou key = pair.Key; value = pair.Value; } + + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + } + + public static void Deconstruct(this DeconstructionBase b, out int a, out int c) + { + a = 1; + c = 2; + } + + public static void Deconstruct(this DeconstructionDerived d, out int a, out int c) + { + a = 3; + c = 4; + } + } + + public class DeconstructionOuter + { + public void Deconstruct(out int x, out DeconstructionDerived d) + { + x = 1; + d = new DeconstructionDerived(); + } } internal class DeconstructionTests @@ -159,6 +194,26 @@ private ref T GetRef() return default((T, T2, T3)); } + private List GetList() + { + return null; + } + + private int GetInt() + { + return 0; + } + + private Tuple GetTupleClass() + { + return null; + } + + private Dictionary GetStringDictionary() + { + return null; + } + private AssignmentTargets Get(int i) { return null; @@ -171,6 +226,15 @@ public void LocalVariable_NoConversion_Custom() Console.WriteLine(myInt4); } + public void LocalVariable_NoConversion_Custom_UnrelatedAssignmentAfter() + { + var (myInt3, myInt4) = GetSource(); + int value = GetInt(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + public void LocalVariable_NoConversion_Tuple() { var (myInt, myInt2) = GetTuple(); @@ -322,6 +386,318 @@ public void LocalVariable_NoConversion_Struct_Custom() Console.WriteLine(value2); } + public void LocalVariable_Nested_ClassInner() + { + var (myInt3, (myInt4, value)) = GetSource>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + + public void LocalVariable_Nested_StructInner() + { + var (myInt3, (myInt4, value)) = GetSource>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + + public void LocalVariable_Nested_StructOuterAndInner() + { + var (myInt3, (myInt4, value)) = GetStructSource>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + + public void LocalVariable_Nested_BothElementsNested() + { + var ((myInt3, value), (myInt4, value2)) = GetSource, StructDeconstructionSource>(); + Console.WriteLine(myInt3); + Console.WriteLine(value); + Console.WriteLine(myInt4); + Console.WriteLine(value2); + } + + public void LocalVariable_Nested_Depth3() + { + var (myInt3, (myInt4, (value, value2))) = GetSource>>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + Console.WriteLine(value2); + } + + public void LocalVariable_Nested_DiscardInnerElement() + { + var (myInt3, (myInt4, _)) = GetSource>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + } + + public void LocalVariable_Nested_SystemTupleSource() + { + var (myInt3, (myInt4, value)) = GetTupleClass>(); + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + + public void LocalVariable_Nested_TupleInner() + { + var (value, (value2, value3)) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + + public void LocalVariable_Nested_TupleInner_Depth3() + { + var (value, (value2, (value3, value4))) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + Console.WriteLine(value4); + } + + public void LocalVariable_Nested_TupleInner_BothElements() + { + var ((value, value2), (value3, value4)) = GetTuple<(int, int), (int, int)>(); + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + Console.WriteLine(value4); + } + + // Both sources are already materialized, so the element stores of the two + // deconstructions are adjacent with nothing in between. Locating the enclosing + // designation of the second one must not walk into the first one's stores. + public void LocalVariable_Nested_TupleInner_AfterAdjacentDeconstruction((int, (int, int)) source, (int, (int, int)) source2) + { + var (value, (value2, value3)) = source; + var (value4, (value5, value6)) = source2; + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + Console.WriteLine(value4); + Console.WriteLine(value5); + Console.WriteLine(value6); + } + + // A statement that is not part of the designation sits between the temporary and + // the reads of it, so the enclosing pattern cannot reach them; they have to be + // reconstructed on their own rather than deferred to a match that never happens. + public void LocalVariable_Nested_TupleInner_BarrierBeforeInnerReads((int, (int, int)) source) + { + (int, int) item = source.Item2; + Console.WriteLine(source.Item1); + var (value, value2) = item; + Console.WriteLine(value); + Console.WriteLine(value2); + } + + // Same, but the barrier sits between the temporary and the outer element read. + public void LocalVariable_Nested_TupleInner_BarrierAfterTemporary((int, (int, int)) source) + { + (int, int) item = source.Item2; + Console.WriteLine(GetInt()); + var (value, _) = source; + var (value2, value3) = item; + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + + // Same, but the preceding statements are plain element reads that keep the inner + // tuple whole, so they are element stores without being a deconstruction. Locating + // the enclosing designation walks back over them; the run they belong to is itself + // a deconstruction, so both are reconstructed. + public void LocalVariable_Nested_TupleInner_AfterAdjacentElementReads((int, (int, int)) source, (int, (int, int)) source2) + { + var (value, tuple2) = source; + var (value2, (value3, value4)) = source2; + Console.WriteLine(value); + Console.WriteLine(tuple2); + Console.WriteLine(value2); + Console.WriteLine(value3); + Console.WriteLine(value4); + } + + public void LocalVariable_Nested_TupleInner_Conversions() + { + int value; + long value2; + long value3; + (value, (value2, value3)) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + + // The element variable escapes the deconstruction, so it must stay a designator + // leaf instead of becoming a nested designation. + public void LocalVariable_TupleInner_ElementUsedOutside() + { +#if OPT + (int, (int, int)) tuple = GetTuple(); + int item = tuple.Item1; + (int, int) item2 = tuple.Item2; + Console.WriteLine(item); + Console.WriteLine(item2.Item1); +#else + var (value, tuple2) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(tuple2.Item1); +#endif + } + + // Same, but the escaping element is in the first position. Every leaf of the + // wrongly nested node precedes the assigned ones there, so the retry that demotes + // it has to be reached before the pattern is judged to start mid-way. + public void LocalVariable_TupleInner_FirstElementUsedOutside() + { + var (tuple2, value) = GetTuple<(int, int), int>(); + Console.WriteLine(tuple2.Item1); + Console.WriteLine(value); + } + + public void ForEach_Nested_TupleInner() + { + foreach (var (value, (value2, value3)) in GetList<(int, (int, int))>()) + { + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + } + + public void LocalVariable_Nested_TypedConversions_UnrelatedCallAfter() + { + long value; + MyInt myInt2; + long value2; + (value, (myInt2, value2)) = GetSource>(); + int value3 = GetInt(); + Console.WriteLine(value); + Console.WriteLine(myInt2); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + + public void LocalVariable_Nested_IntToLongConversion() + { + int value; + MyInt myInt2; + long value2; + (value, (myInt2, value2)) = GetSource>(); + Console.WriteLine(value); + Console.WriteLine(myInt2); + Console.WriteLine(value2); + } + + public void LocalVariable_Nested_ElementDeconstructedAfterBarrier() + { + GetSource>().Deconstruct(out var a, out var b); + Console.WriteLine(a); + var (myInt2, value) = b; + Console.WriteLine(myInt2); + Console.WriteLine(value); + } + + public void LocalVariable_Nested_OuterElementUsedTwice() + { + GetSource>().Deconstruct(out var a, out var b); + var (myInt2, value) = b; + Console.WriteLine(a); + Console.WriteLine(a); + Console.WriteLine(myInt2); + Console.WriteLine(value); + } + + public void ForEach_Nested() + { + foreach (var (myInt3, (myInt4, value)) in GetList>>()) + { + Console.WriteLine(myInt3); + Console.WriteLine(myInt4); + Console.WriteLine(value); + } + } + + public void ForEach_Nested_KeyValuePair() + { + foreach (var (value, (myInt2, value2)) in GetStringDictionary>()) + { + Console.WriteLine(value); + Console.WriteLine(myInt2); + Console.WriteLine(value2); + } + } + + public void Property_Nested_NoConversion() + { + (Get(0).Int, (Get(1).My, Get(2).String)) = GetSource>(); + } + + public void Property_Nested_IntToLongConversion() + { + (Get(0).Int, (Get(1).My, Get(2).Long)) = GetSource>(); + } + + public void Property_Nested_DiscardInnerElement() + { + (Get(0).NMy, (_, Get(1).My)) = GetSource>(); + } + + public unsafe void Pointer_NoConversion_Tuple(int* p) + { + int value; + (*p, value) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value); + } + + // The store opcode is sign-agnostic - stind.i4 reports int for a uint target and + // stind.i1 reports sbyte for a byte one - so the element type of the target cannot + // be taken from it: doing so refuses every one of these deconstructions. + // The IL calls the extension declared on the base type, forced by the cast. Folding + // this into a nested designation would rebind Deconstruct on the element's static + // type, where the extension declared on the derived type wins and returns different + // values, so the call has to stay explicit. + public void Nested_CompetingExtensionDeconstruct(DeconstructionOuter o) + { + o.Deconstruct(out var x, out var d); + ((DeconstructionBase)d).Deconstruct(out int a, out int c); + Console.WriteLine(x); + Console.WriteLine(a); + Console.WriteLine(c); + } + + public unsafe void Pointer_NoConversion_Tuple_UInt(uint* p) + { + int value; + (*p, value) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value); + } + + public unsafe void Pointer_NoConversion_Tuple_Byte(byte* p) + { + int value; + (*p, value) = GetTuple(); + Console.WriteLine(value); + Console.WriteLine(value); + } + + public unsafe void Pointer_Nested_Custom(int* p) + { + MyInt myInt2; + int value; + (*p, (myInt2, value)) = GetSource>(); + Console.WriteLine(myInt2); + Console.WriteLine(value); + } + public void Property_NoConversion_Custom() { (Get(0).NMy, Get(1).My) = GetSource(); diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs index f2993a7f84..3d13f228da 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs @@ -259,7 +259,14 @@ internal static bool IsAssignment(ILInstruction inst, ICompilation typeSystem, o if (stobj.Target.InferType(typeSystem) is ByReferenceType brt) expectedType = brt.ElementType; else - expectedType = SpecialType.UnknownType; + { + // Pointer targets do not infer a ByReferenceType. stobj.Type cannot stand + // in for the element type: it comes from the store opcode, which is + // sign-agnostic, so a uint* and an int* both report int32 and a byte* + // reports sbyte. Recover the declared type instead, and only fall back to + // the store where the target is not a pointer at all. + expectedType = GetPointerElementType(stobj.Target, typeSystem) ?? stobj.Type; + } value = stobj.Value; return true; default: @@ -267,6 +274,28 @@ internal static bool IsAssignment(ILInstruction inst, ICompilation typeSystem, o } } + /// + /// The element type of a pointer-typed target. A pointer passing through a stack slot + /// is typed IntPtr there, so the declared type has to be taken from the definition the + /// slot was filled from. Returns null if the target is not a pointer. + /// + static IType GetPointerElementType(ILInstruction target, ICompilation typeSystem) + { + // Bounded because a definition chain could be cyclic in invalid IL. + for (int step = 0; step < 4; step++) + { + if (target.InferType(typeSystem) is PointerType pointerType) + return pointerType.ElementType; + if (!target.MatchLdLoc(out var v) || !v.IsSingleDefinition + || v.StoreInstructions.Count != 1 || !(v.StoreInstructions[0] is StLoc store)) + { + return null; + } + target = store.Value; + } + return null; + } + internal override void CheckInvariant(ILPhase phase) { base.CheckInvariant(phase); diff --git a/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs index f9938f3ed4..63324af82d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs @@ -16,12 +16,13 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +#nullable enable + using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Resources; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.TypeSystem; @@ -30,40 +31,56 @@ namespace ICSharpCode.Decompiler.IL.Transforms { /// - /// + /// Detects that a run of statements is a lowered deconstruction assignment - rooted in a + /// Deconstruct call or in tuple element reads, including nested designations - and folds + /// it into a single DeconstructInstruction. /// + /* + 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) + } + } + + A nested designation over Deconstruct calls (var (x, (a, b)) = o;) chains the calls, + with a defensive copy for struct elements: + call Deconstruct(ldloc o, ldloca x', ldloca inner) + call Deconstruct(ldloca inner, ldloca a', ldloca b') + ...conversions/assignments over the leaves x', a', b'... + + A nested designation over tuples (var (x, (a, b)) = t;) is lowered to one temporary + per nested designation, followed by element reads in depth-first leaf order: + stloc inner(ldobj(ldflda Item2(ldloca t))) + stloc x(ldobj(ldflda Item1(ldloca t))) + stloc a(ldobj(ldflda Item1(ldloca inner))) + stloc b(ldobj(ldflda Item2(ldloca inner))) + * */ class DeconstructionTransform : IStatementTransform { - StatementTransformContext context; + StatementTransformContext context = null!; 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) - } - } - * */ + readonly Dictionary tupleNodes = new Dictionary(); + ILVariable?[] deconstructionResults = null!; + TupleNode? tupleRoot; + bool rootedInDeconstructCall; + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) { if (!context.Settings.Deconstruction) @@ -81,7 +98,7 @@ void IStatementTransform.Run(Block block, int pos, StatementTransformContext con } finally { - this.context = null; + this.context = null!; Reset(); } } @@ -89,62 +106,147 @@ void IStatementTransform.Run(Block block, int pos, StatementTransformContext con private void Reset() { this.deconstructionResultsLookup.Clear(); - this.tupleVariable = null; - this.tupleType = null; - this.deconstructionResults = null; + this.tupleNodes.Clear(); + this.tupleRoot = null; + this.deconstructionResults = null!; + this.rootedInDeconstructCall = false; } - struct ConversionInfo + /// + /// call Deconstruct(target, ldloca out0, ...) [+ nested Deconstruct calls] + /// | stloc temp(ldobj(ldflda ItemN(ldloca tuple))) ... [nested tuple designations] + /// stloc conv0(conv(...)) ... + /// assignments ... + /// => + /// deconstruct { init: pattern: conversions: assignments: } (see class comment) + /// + bool TransformDeconstruction(Block block, int pos) { - public IType inputType; - public Conv conv; + int startPos = pos; + // Blocks are processed back to front, so the inner parts of a nested deconstruction + // are visited before the position its matching starts at; matching them on their own + // would consume the pattern piecemeal. Defer to the enclosing attempt where one + // exists (see the guard for the precision guarantees). + if (IsConsumableByEnclosingDeconstruction(block, pos)) + return false; + if (!MatchDeconstructionSequence(block, startPos, out pos, out var rootCall, + out var rootTestedOperand, out var conversionStLocs, out var delayedActions)) + { + return false; + } + context.Step("Deconstruction", block.Instructions[startPos]); + DeconstructInstruction replacement = new DeconstructInstruction(); + IMethod? deconstructMethod = rootCall?.Method; + IType deconstructedType; + if (deconstructMethod == null) + { + deconstructedType = tupleRoot!.Type; + rootTestedOperand = new LdLoc(tupleRoot.Variable); + } + else + { + if (deconstructMethod.IsStatic) + { + deconstructedType = deconstructMethod.Parameters[0].Type; + } + else + { + deconstructedType = deconstructMethod.DeclaringType; + } + } + var rootTempVariable = context.Function.RegisterVariable(VariableKind.PatternLocal, deconstructedType); + if (rootCall != null) + { + replacement.Pattern = BuildPatternMatch(rootCall, rootTempVariable, rootTestedOperand!); + } + else + { + replacement.Pattern = BuildTuplePatternMatch(tupleRoot!, rootTempVariable, rootTestedOperand!); + } + 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); + context.EndStep(replacement); + return true; } /// - /// Get index of deconstruction result or tuple element - /// Returns -1 on failure. + /// Matches the full statement sequence of one deconstruction, starting at startPos: + /// [Deconstruct call + nested calls | nested tuple designation temporaries] + /// [conversions] + /// [assignments] + /// On success, endPos is the position after the last consumed statement. + /// The block is not modified; all rewrites are accumulated in delayedActions. /// - int FindIndex(ILInstruction inst, out Action delayedActions) + bool MatchDeconstructionSequence(Block block, int startPos, out int endPos, + out DeconstructionCall? rootCall, out ILInstruction? rootTestedOperand, + out List conversionStLocs, 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 _)) + HashSet? doNotNest = null; + while (true) { - 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) + Reset(); + endPos = startPos; + int pos = startPos; + delayedActions = null; + MatchDeconstruction(block, ref pos, out rootCall, out rootTestedOperand); + if (rootCall == null) + MatchNestedTupleDesignations(block, ref pos, doNotNest); + if (!MatchConversions(block, ref pos, out var conversions, out conversionStLocs, ref delayedActions)) + return false; + if (!MatchAssignments(block, ref pos, conversions, conversionStLocs, ref delayedActions, + allowUnrelatedAssignments: rootCall != null, out bool anyAssignments)) { - this.tupleVariable = v; - this.tupleType = (TupleType)tupleType; - this.deconstructionResults = new ILVariable[this.tupleType.Cardinality]; + return false; } - if (this.tupleType.Cardinality < 2) - return -1; - if (v != tupleVariable || !this.tupleType.Equals(tupleType)) - return -1; - if (this.deconstructionResults[index] == null) + // Without any assignment the statement is a plain Deconstruct call, unless a nested + // deconstruction was consumed: then all leaves are single-use elements handled by + // the forwarding fixup in MatchAssignments. + if (!anyAssignments && !(rootCall != null && rootCall.NestedCalls.Any(c => c != null))) + return false; + // A nested tuple designation only holds if the pattern consumed every read of + // its temporary; a remaining read means the value escapes the designation. + // Retry with the variable as a plain designator leaf, which restores the flat + // deconstruction the escaping read needs. This has to be decided before the + // leaf check below: every leaf of a wrongly nested first element precedes the + // assigned ones, so that check would report the pattern as starting mid-way + // and give up on a designation the retry can still make work. + var escaped = EscapedTupleNodes(); + if (escaped != null) { - var freshVar = new ILVariable(VariableKind.StackSlot, this.tupleType.ElementTypes[index]) { Name = "E_" + index }; - delayedActions += _ => context.Function.Variables.Add(freshVar); - this.deconstructionResults[index] = freshVar; + doNotNest ??= new HashSet(); + doNotNest.UnionWith(escaped); + continue; } - delayedActions += _ => { - inst.ReplaceWith(new LdLoc(this.deconstructionResults[index])); - }; - return index; + // 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; + endPos = pos; + return true; + } + + List? EscapedTupleNodes() + { + List? escaped = null; + foreach (var node in tupleNodes.Values) + { + if (node == tupleRoot) + continue; + if (node.MatchedAccessCount != node.Variable.LoadCount + node.Variable.AddressCount) + { + escaped ??= new List(); + escaped.Add(node.Variable); + } + } + return escaped; } - return -1; } /// @@ -157,7 +259,7 @@ 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)) + if (!(v.IsSingleDefinition && v.LoadInstructions is [var loadInst])) return false; if (pos + 1 >= block.Instructions.Count) return false; @@ -165,7 +267,6 @@ bool InlineDeconstructionInitializer(Block block, int pos) 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) @@ -176,8 +277,7 @@ bool InlineDeconstructionInitializer(Block block, int pos) if (deconstruction.Init.Count > 0) { var a = deconstruction.Init[0].Variable.LoadInstructions.Single(); - var b = v.LoadInstructions.Single(); - if (!b.IsBefore(a)) + if (!loadInst.IsBefore(a)) return false; } context.Step("InlineDeconstructionInitializer", block.Instructions[pos]); @@ -187,126 +287,459 @@ bool InlineDeconstructionInitializer(Block block, int pos) return true; } - bool TransformDeconstruction(Block block, int pos) + /// + /// Whether the statement at pos belongs to a deconstruction whose matching starts at an + /// earlier position in the block, in either nesting shape: + /// + /// call Deconstruct(..., ldloca inner, ...) at enclosingPos + /// [stloc copy(ldloc inner)] defensive copy of a struct element + /// call Deconstruct(ldloc(a) inner|copy, ...) at pos + /// + /// stloc temp(ldobj(ldflda ItemN(ldloc(a) outer))) earlier in the block + /// ... + /// stloc x([conv](ldobj(ldflda ItemK(ldloc(a) temp)))) at pos + /// + /// The chained calls are emitted back to back, so an enclosing call that is not the + /// preceding statement has something between it and pos that stops it from reaching + /// here; the deconstruction at pos is then matched on its own. Nested designation + /// temporaries are stored before the enclosing run's own element reads, so the two are + /// not adjacent and only the store has to be found. + /// + /// Deferring is worth it only if the enclosing attempt can succeed, so the constraint + /// MatchDeconstructionCall places on out-parameters is checked here as well: without it + /// an element used more than once would defer this position to an attempt that then + /// rejects the call, and the back-to-front walk gives it no second chance. + /// + /// Getting this wrong costs sugar, never correctness: the statement at pos is either + /// folded into the enclosing deconstruction or decompiled as the explicit calls and + /// element reads it came from. + /// + bool IsConsumableByEnclosingDeconstruction(Block block, int pos) { - int startPos = pos; - Action delayedActions = null; - if (MatchDeconstruction(block.Instructions[pos], out IMethod deconstructMethod, - out ILInstruction rootTestedOperand)) + if (TryFindEnclosingDeconstructionCall(block, pos, out int enclosingPos)) { - pos++; + if (enclosingPos != pos - 1 + && !(enclosingPos == pos - 2 && block.Instructions[pos - 1] is StLoc { Value: LdLoc })) + { + return false; + } + var enclosingCall = (CallInstruction)block.Instructions[enclosingPos]; + for (int i = 1; i < enclosingCall.Arguments.Count; i++) + { + if (!enclosingCall.Arguments[i].MatchLdLoca(out var outParam) + || !(outParam.StoreCount == 0 && outParam.AddressCount == 1 && outParam.LoadCount <= 1)) + { + return false; + } + } + return true; + } + return HasEnclosingTupleDesignation(block, pos); + } + + /// + /// call Deconstruct(..., ldloca v, ...) at enclosingPos + /// [stloc copy(ldloc v)] defensive copy of a struct element + /// ... + /// call Deconstruct(ldloc(a) v|copy, ...) at pos + /// + static bool TryFindEnclosingDeconstructionCall(Block block, int pos, out int enclosingPos) + { + enclosingPos = -1; + if (!(block.Instructions[pos] is CallInstruction call)) + return false; + if (!MatchInstruction.IsDeconstructMethod(call.Method) || call.Arguments.Count == 0) + return false; + var target = call.Arguments[0]; + if (!MatchLdLocOrLdLoca(target, out var v)) + return false; + // look through the defensive copy of a struct element + if (v.StoreInstructions is [StLoc copy] && copy.Value.MatchLdLoc(out var copySource)) + { + v = copySource; + } + // StoreCount also counts the initial value of parameters, on purpose + if (v.StoreCount != 0) + return false; + if (!(v.AddressInstructions is [{ Parent: CallInstruction enclosingCall } addressLoad] + && addressLoad.ChildIndex > 0 + && MatchInstruction.IsDeconstructMethod(enclosingCall.Method))) + { + return false; } - if (!MatchConversions(block, ref pos, out var conversions, out var conversionStLocs, ref delayedActions)) + if (enclosingCall.Parent != block) return false; + enclosingPos = enclosingCall.ChildIndex; + return enclosingPos >= 0 && enclosingPos < pos; + } - if (!MatchAssignments(block, ref pos, conversions, conversionStLocs, ref delayedActions)) + /// + /// stloc temp(ldobj(ldflda ItemN(ldloc(a) outer))) earlier in the block + /// ... + /// stloc x([conv](ldobj(ldflda ItemK(ldloc(a) temp)))) at pos + /// The statement at pos reads an element of a tuple stored by an earlier statement that + /// is itself an element read, i.e. a candidate nested designation temporary. + /// + static bool HasEnclosingTupleDesignation(Block block, int pos) + { + if (!block.Instructions[pos].MatchStLoc(out _, out var value)) 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) + if (value is Conv conv) + value = conv.Argument; + if (!MatchTupleElementRead(value, out var container, out _, out _)) return false; - context.Step("Deconstruction", block.Instructions[startPos]); - DeconstructInstruction replacement = new DeconstructInstruction(); - IType deconstructedType; - if (deconstructMethod == null) + if (!(container.StoreInstructions is [StLoc store]) || store.Parent != block) + return false; + if (!MatchTupleElementStore(store, out _, out _, out _, out _)) + return false; + if (store.ChildIndex >= pos) + return false; + // The temporaries and element reads of one designation are stored back to back. + // A statement of any other kind in between stops the enclosing pattern from + // reaching this position, and deferring to it would lose the deconstruction here + // as well, because the back-to-front walk does not come back. + for (int between = store.ChildIndex + 1; between < pos; between++) { - deconstructedType = this.tupleType; - rootTestedOperand = new LdLoc(this.tupleVariable); + if (!MatchTupleElementStore(block.Instructions[between], out _, out _, out _, out _)) + return false; } - else + return true; + } + + /// + /// A matched Deconstruct call: one node of the (possibly nested) deconstruction pattern. + /// + sealed class DeconstructionCall + { + public IMethod Method = null!; + /// Pattern variable of this match node; null for the root (which gets a fresh temp). + public ILVariable? Receiver; + /// The out-argument variable per element. + public ILVariable[] Results = null!; + /// Nested deconstruction per element; null = leaf element. + public DeconstructionCall?[] NestedCalls = null!; + } + + /// + /// call Deconstruct(target, ldloca x, ldloca inner) the root call, at pos + /// [nested Deconstruct calls, see MatchNestedDeconstructions] + /// On success, the leaf out-variables carry flat indices in depth-first order: this is + /// the order in which StatementBuilder/ExpressionBuilder pair pattern variables with + /// assignments, so the index checks in MatchConversions/MatchAssignments work unchanged + /// for nested patterns. + /// + void MatchDeconstruction(Block block, ref int pos, out DeconstructionCall? rootCall, + out ILInstruction? testedOperand) + { + rootCall = MatchDeconstructionCall(block.Instructions[pos], out testedOperand); + if (rootCall == null) + return; + rootedInDeconstructCall = true; + pos++; + MatchNestedDeconstructions(block, ref pos, rootCall); + // Assign flat indices to the leaves in depth-first order: this is the order in which + // StatementBuilder/ExpressionBuilder pair pattern variables with assignments, so the + // index checks in MatchConversions/MatchAssignments work unchanged for nested patterns. + var leaves = new List(); + CollectLeaves(rootCall, leaves); + deconstructionResults = leaves.ToArray(); + for (int i = 0; i < deconstructionResults.Length; i++) { - if (deconstructMethod.IsStatic) - { - deconstructedType = deconstructMethod.Parameters[0].Type; - } - else - { - deconstructedType = deconstructMethod.DeclaringType; - } + deconstructionResultsLookup.Add(deconstructionResults[i]!, i); } - 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) + + static void CollectLeaves(DeconstructionCall call, List leaves) { - 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 + for (int i = 0; i < call.Results.Length; i++) { - result.Kind = VariableKind.PatternLocal; + if (call.NestedCalls[i] is DeconstructionCall nested) + CollectLeaves(nested, leaves); + else + leaves.Add(call.Results[i]); } - 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); - context.EndStep(replacement); - return true; } - bool MatchDeconstruction(ILInstruction inst, out IMethod deconstructMethod, - out ILInstruction testedOperand) + /// + /// call(virt) Deconstruct(target, ldloca out0, ldloca out1, ...) + /// where every out-argument is a single-use temporary. + /// + DeconstructionCall? MatchDeconstructionCall(ILInstruction inst, out ILInstruction? testedOperand) { testedOperand = null; - deconstructMethod = null; - deconstructionResults = null; if (!(inst is CallInstruction call)) - return false; + return null; if (!MatchInstruction.IsDeconstructMethod(call.Method)) - return false; + return null; if (call.Method.IsStatic || call.Method.DeclaringType.IsReferenceType == false) { if (!(call is Call)) - return false; + return null; } else { if (!(call is CallVirt)) - return false; + return null; } if (call.Arguments.Count < 3) - return false; - deconstructionResults = new ILVariable[call.Arguments.Count - 1]; - for (int i = 0; i < deconstructionResults.Length; i++) + return null; + var results = new ILVariable[call.Arguments.Count - 1]; + for (int i = 0; i < results.Length; i++) { if (!call.Arguments[i + 1].MatchLdLoca(out var v)) - return false; + return null; // 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; + return null; + results[i] = v; } testedOperand = call.Arguments[0]; - deconstructMethod = call.Method; - return true; + return new DeconstructionCall { + Method = call.Method, + Results = results, + NestedCalls = new DeconstructionCall[results.Length] + }; + } + + /// + /// Per element of the parent call, in order: + /// [stloc copy(ldloc result)] defensive copy for a struct element + /// call Deconstruct(ldloc(a) result|copy, ldloca ...) recursing into its elements + /// C# evaluates nested Deconstruct calls left-to-right, directly after the parent call, + /// before any conversions or assignments: the elements are visited depth-first, and the + /// stack of pending elements takes the place of recursing into a matched nested call. + /// + void MatchNestedDeconstructions(Block block, ref int pos, DeconstructionCall rootCall) + { + var pendingElements = new Stack<(DeconstructionCall Call, int ElementIndex)>(); + pendingElements.Push((rootCall, 0)); + while (pendingElements.Count > 0) + { + var (parent, i) = pendingElements.Pop(); + if (i + 1 < parent.Results.Length) + pendingElements.Push((parent, i + 1)); + ILVariable result = parent.Results[i]; + int savedPos = pos; + ILVariable receiver = result; + var inst = block.Instructions.ElementAtOrDefault(pos); + if (inst != null && inst.MatchStLoc(out var copy, out var copiedValue) + && copiedValue.MatchLdLoc(result) + && copy.StoreCount == 1 + && copy.LoadCount + copy.AddressCount == 1) + { + receiver = copy; + pos++; + inst = block.Instructions.ElementAtOrDefault(pos); + } + var nested = inst == null ? null : MatchDeconstructionCall(inst, out _); + if (nested == null || !IsReceiverReference(((CallInstruction)inst!).Arguments[0], receiver)) + { + pos = savedPos; + continue; + } + if (receiver != result && result.LoadCount != 1) + { + // the copy must be the element's only use + pos = savedPos; + continue; + } + if (!BindsOnElementType(nested.Method, result.Type)) + { + // A nested designation rebinds Deconstruct on the element's static type + // when recompiled; if that picks a different method (member hiding), the + // call must stay explicit, where a cast can preserve the binding. + pos = savedPos; + continue; + } + pos++; + nested.Receiver = receiver; + parent.NestedCalls[i] = nested; + // its elements are evaluated before the parent's remaining ones + pendingElements.Push((nested, 0)); + } + + static bool IsReceiverReference(ILInstruction target, ILVariable receiver) + { + return MatchLdLocOrLdLoca(target, out var v) && v == receiver; + } + + static bool BindsOnElementType(IMethod method, IType elementType) + { + int outParamCount = method.Parameters.Count - (method.IsStatic ? 1 : 0); + IType type = elementType; + while (type != null) + { + if (!method.IsStatic && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(type, method.DeclaringType)) + return true; + if (type.GetMethods(m => m.Name == "Deconstruct", GetMemberOptions.IgnoreInheritedMembers) + .Any(m => !m.IsStatic && m.Parameters.Count == outParamCount)) + { + // An instance Deconstruct of the same arity is declared on a type more + // derived than the called method's declaring type: it hides the called + // method (and wins over a called extension method). + return false; + } + type = type.DirectBaseTypes.FirstOrDefault(t => t.Kind == TypeKind.Class)!; + } + // The chain ended without seeing the declaring type, so an instance method's + // binding cannot be verified. An extension method is reached by its receiver + // type, and one declared on a more derived type wins over it; which extensions + // are in scope where the output is compiled is not known here, so the binding + // is only certain when the element type is the receiver type itself. + return method.IsStatic + && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(elementType, method.Parameters[0].Type); + } + } + + /// + /// A tuple variable being deconstructed: one node of the (possibly nested) designation. + /// Nested nodes are the temporaries a nested tuple designation is lowered to. + /// + sealed class TupleNode + { + public readonly ILVariable Variable; + public readonly TupleType Type; + /// Nested designation per element; null = leaf element. + public readonly TupleNode[] NestedElements; + /// Flat leaf index (depth-first) of each element. + public int[] ElementFlatIndex = null!; + /// Number of element reads of consumed by the pattern. + public int MatchedAccessCount; + + public TupleNode(ILVariable variable, TupleType type) + { + Variable = variable; + Type = type; + NestedElements = new TupleNode[type.Cardinality]; + } } + /// + /// stloc temp(ldobj(ldflda ItemN(ldloc(a) container))) one per nested designation + /// ... + /// The temporaries a nested tuple designation is lowered to: parents before children, + /// all evaluated before any conversions or assignments. The consumed variables form the + /// tuple node tree rooted at the outermost tuple. + /// + void MatchNestedTupleDesignations(Block block, ref int pos, HashSet? doNotNest) + { + while (MatchTupleElementStore(block.Instructions.ElementAtOrDefault(pos), + out var temp, out var container, out var containerType, out int index)) + { + if (doNotNest != null && doNotNest.Contains(temp)) + break; + if (!(temp.StoreCount == 1 && temp.LoadCount + temp.AddressCount >= 1)) + break; + // Every use of the temporary must itself be a tuple element read, + // 'ldobj(ldflda ItemK(...temp...))', so that the pattern can consume them all; + // reads it does not consume are rejected by the escape check afterwards. + if (!AllUsesAreTupleElementReads(temp)) + break; + var containerNode = ResolveTupleContainer(container, containerType); + if (containerNode == null) + break; + if (index >= containerNode.NestedElements.Length || containerNode.NestedElements[index] != null) + break; + // The container's element type is authoritative for the temporary's tuple type: + // a stack slot's own type can be imprecise. A temporary with a precise type must + // agree with the element type. + var elementType = containerNode.Type.ElementTypes[index]; + if (TupleType.GetTupleElementTypes(elementType).IsDefaultOrEmpty) + break; + var tempType = TupleType.FromUnderlyingType(context.TypeSystem, elementType); + if (tempType == null || tempType.Cardinality < 2) + break; + if (!TupleType.GetTupleElementTypes(temp.Type).IsDefaultOrEmpty + && !NormalizeTypeVisitor.TypeErasure.EquivalentTypes(elementType, temp.Type)) + { + break; + } + var node = new TupleNode(temp, tempType); + containerNode.NestedElements[index] = node; + // The temporary's store reads one element of the container. + containerNode.MatchedAccessCount++; + this.tupleNodes.Add(temp, node); + InitializeFlatLeafIndices(); + pos++; + } + + static bool AllUsesAreTupleElementReads(ILVariable temp) + { + foreach (var use in temp.AddressInstructions.Concat(temp.LoadInstructions)) + { + if (!(use.Parent is LdFlda elementAccess && elementAccess.Parent is LdObj)) + return false; + } + return true; + } + } + + /// + /// Resolves the container of a tuple element access against the tree of tuple nodes; + /// the first access establishes its container as the root. Returns null if the + /// container is not part of the tree or its type does not fit a deconstruction. + /// + TupleNode? ResolveTupleContainer(ILVariable container, IType containerType) + { + var normalizedType = TupleType.FromUnderlyingType(context.TypeSystem, containerType); + if (normalizedType == null || normalizedType.Cardinality < 2) + return null; + if (tupleRoot == null) + { + tupleRoot = new TupleNode(container, normalizedType); + tupleNodes.Add(container, tupleRoot); + InitializeFlatLeafIndices(); + } + if (!tupleNodes.TryGetValue(container, out var node)) + return null; + return node.Type.Equals(normalizedType) ? node : null; + } + + /// + /// Assigns depth-first flat leaf indices to every element of the tuple node tree and + /// allocates the flat results array. Depth-first order is the order in which the + /// consumers pair pattern variables with conversions and assignments. Called whenever + /// the tree grows; the results array is still empty then, because the tree is complete + /// before MatchConversions/MatchAssignments start populating it. + /// + void InitializeFlatLeafIndices() + { + int totalLeaves = AssignFlatIndices(tupleRoot!, 0); + this.deconstructionResults = new ILVariable[totalLeaves]; + + static int AssignFlatIndices(TupleNode node, int nextLeafIndex) + { + node.ElementFlatIndex = new int[node.Type.Cardinality]; + for (int i = 0; i < node.Type.Cardinality; i++) + { + node.ElementFlatIndex[i] = nextLeafIndex; + if (node.NestedElements[i] != null) + nextLeafIndex = AssignFlatIndices(node.NestedElements[i], nextLeafIndex); + else + nextLeafIndex++; + } + return nextLeafIndex; + } + } + + struct ConversionInfo + { + public IType? inputType; + public Conv? conv; + } + + /// + /// stloc conv0(conv(FindIndex-resolvable value)) + /// stloc conv1(conv(...)) + /// ... + /// The run of single-use conversion temporaries following the deconstruction, in flat + /// leaf index order. + /// bool MatchConversions(Block block, ref int pos, out Dictionary conversions, out List conversionStLocs, - ref Action delayedActions) + ref Action? delayedActions) { conversions = new Dictionary(); conversionStLocs = new List(); @@ -330,11 +763,17 @@ bool MatchConversions(Block block, ref int pos, return true; } - bool MatchConversion(ILInstruction inst, out ILInstruction inputInstruction, - out ILVariable outputVariable, out ConversionInfo info) + /// + /// stloc output(conv(input)) + /// + bool MatchConversion(ILInstruction? inst, [NotNullWhen(true)] out ILInstruction? inputInstruction, + [NotNullWhen(true)] out ILVariable? outputVariable, out ConversionInfo info) { info = default; inputInstruction = null; + outputVariable = null; + if (inst == null) + return false; if (!inst.MatchStLoc(out outputVariable, out var value)) return false; if (!(value is Conv conv)) @@ -347,17 +786,37 @@ bool MatchConversion(ILInstruction inst, out ILInstruction inputInstruction, return true; } + /// + /// assignment(FindIndex-resolvable value) see MatchAssignment + /// ... + /// The run of assignments following the conversions, in flat leaf index order. + /// Single-use elements without an assignment are forwarded through a fresh variable + /// assigned inside the deconstruction. + /// bool MatchAssignments(Block block, ref int pos, Dictionary conversions, List conversionStLocs, - ref Action delayedActions) + ref Action? delayedActions, + bool allowUnrelatedAssignments, + out bool anyAssignments) { + anyAssignments = false; 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 < 0 && allowUnrelatedAssignments) + { + // For a Deconstruct call the element list is fixed by the call's + // out-arguments, so an assignment whose value is unrelated to the + // deconstruction just ends the pattern and stays after the deconstruct + // instruction. (For tuples the elements are discovered from the + // assignments, so ending early would misread a suffix as the pattern: + // keep rejecting there.) + break; + } if (index <= previousIndex) return false; AddMissingAssignmentsForConversions(index, ref delayedActions); @@ -374,7 +833,7 @@ bool MatchAssignments(Block block, ref int pos, && conversionInfo.conv == null) { delayedActions += _ => { - assignmentTarget.Type = conversionInfo.inputType; + assignmentTarget.Type = conversionInfo.inputType!; }; } else @@ -432,7 +891,8 @@ bool MatchAssignments(Block block, ref int pos, } } - return startPos != pos; + anyAssignments = startPos != pos; + return true; int GetAssignmentIndex(ILInstruction inst) { @@ -449,7 +909,7 @@ int GetAssignmentIndex(ILInstruction inst) return int.MaxValue; } - void AddMissingAssignmentsForConversions(int index, ref Action delayedActions) + void AddMissingAssignmentsForConversions(int index, ref Action? delayedActions) { while (conversionStLocIndex < conversionStLocs.Count) { @@ -472,7 +932,13 @@ void AddMissingAssignmentsForConversions(int index, ref Action addAssignment) + /// + /// stloc v(value) | stobj(target, value) | call set_Property(target, value) + /// or the result-used form + /// stloc s(Block CallInlineAssign { call set_Property(target, stloc tmp(value)); final: ldloc tmp }) + /// where the setter call is moved into the assignments block. + /// + bool MatchAssignment(ILInstruction? inst, [NotNullWhen(true)] out IType? targetType, [NotNullWhen(true)] out ILInstruction? valueInst, [NotNullWhen(true)] out Action? addAssignment) { targetType = null; valueInst = null; @@ -507,6 +973,57 @@ bool MatchAssignment(ILInstruction inst, out IType targetType, out ILInstruction } } + /// + /// ldloc result a registered result or conversion output + /// ldobj(ldflda ItemN(ldloc(a) v)) an element read on the tuple node tree + /// Resolves the value of a conversion or assignment to its flat leaf index. + /// 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 (!MatchTupleElementRead(inst, out var container, out var containerType, out int elementIndex)) + return -1; + if (rootedInDeconstructCall) + { + // A pattern rooted in a Deconstruct call must not absorb tuple element + // accesses: discovering the tuple here would overwrite the call's result + // bookkeeping and destroy the rewritten tuple access on failure. + return -1; + } + var node = ResolveTupleContainer(container, containerType); + if (node == null) + return -1; + if (elementIndex >= node.NestedElements.Length || node.NestedElements[elementIndex] != null) + { + // The element is bound to a nested designation; a direct read of it would + // be a second consumption of the same element. + return -1; + } + int flatIndex = node.ElementFlatIndex[elementIndex]; + node.MatchedAccessCount++; + if (this.deconstructionResults[flatIndex] == null) + { + var freshVar = new ILVariable(VariableKind.StackSlot, node.Type.ElementTypes[elementIndex]) { Name = "E_" + flatIndex }; + delayedActions += _ => context.Function.Variables.Add(freshVar); + this.deconstructionResults[flatIndex] = freshVar; + } + delayedActions += _ => { + inst.ReplaceWith(new LdLoc(this.deconstructionResults[flatIndex]!)); + }; + return flatIndex; + } + + /// + /// Gets whether the matched conv instruction (or its absence) is the lowering of the + /// implicit conversion from the input type to the assignment's target type. + /// bool IsCompatibleImplicitConversion(IType targetType, ConversionInfo conversionInfo) { var c = CSharpConversions.Get(context.TypeSystem) @@ -537,5 +1054,141 @@ bool IsCompatibleImplicitConversion(IType targetType, ConversionInfo conversionI } return false; } + + /// + /// Builds, recursing into nested calls: + /// match.deconstruct[Method] (matchVariable = testedOperand) { + /// match(result_i = deconstruct.result i(ldloc matchVariable)), + /// match.deconstruct[...] (receiver_j = deconstruct.result j(ldloc matchVariable)) { ... } + /// } + /// + MatchInstruction BuildPatternMatch(DeconstructionCall call, ILVariable matchVariable, ILInstruction testedOperand) + { + matchVariable.Kind = VariableKind.PatternLocal; + var match = new MatchInstruction(matchVariable, call.Method, testedOperand) { + IsDeconstructCall = true + }; + for (int i = 0; i < call.Results.Length; i++) + { + var nested = call.NestedCalls[i]; + if (nested != null) + { + var receiver = nested.Receiver!; + match.SubPatterns.Add(BuildPatternMatch(nested, receiver, + new DeconstructResultInstruction(i, receiver.StackType, new LdLoc(matchVariable)))); + } + else + { + var result = call.Results[i]; + result.Kind = VariableKind.PatternLocal; + match.SubPatterns.Add( + new MatchInstruction( + result, + new DeconstructResultInstruction(i, result.StackType, new LdLoc(matchVariable)) + ) + ); + } + } + return match; + } + + /// + /// Builds, recursing into nested designations: + /// match.tuple (matchVariable = testedOperand) { + /// match(result_i = deconstruct.result i(ldloc matchVariable)), + /// match.tuple (temp_j = deconstruct.result j(ldloc matchVariable)) { ... } + /// } + /// Unassigned leaf elements get a fresh, load-free pattern variable (a discard). + /// + MatchInstruction BuildTuplePatternMatch(TupleNode node, ILVariable matchVariable, ILInstruction testedOperand) + { + matchVariable.Kind = VariableKind.PatternLocal; + var match = new MatchInstruction(matchVariable, method: null, testedOperand) { + IsDeconstructTuple = true + }; + for (int i = 0; i < node.Type.Cardinality; i++) + { + var nested = node.NestedElements[i]; + if (nested != null) + { + // A stack-slot temporary can have an imprecise type; the match variable of + // a tuple pattern must have the tuple type. + if (TupleType.GetTupleElementTypes(nested.Variable.Type).IsDefaultOrEmpty) + nested.Variable.Type = nested.Type; + match.SubPatterns.Add(BuildTuplePatternMatch(nested, nested.Variable, + new DeconstructResultInstruction(i, nested.Variable.StackType, new LdLoc(matchVariable)))); + } + else + { + int flatIndex = node.ElementFlatIndex[i]; + var result = deconstructionResults[flatIndex]; + if (result == null) + { + var freshVar = new ILVariable(VariableKind.PatternLocal, node.Type.ElementTypes[i]) { Name = "E_" + flatIndex }; + context.Function.Variables.Add(freshVar); + result = freshVar; + } + else + { + result.Kind = VariableKind.PatternLocal; + } + match.SubPatterns.Add( + new MatchInstruction( + result, + new DeconstructResultInstruction(i, result.StackType, new LdLoc(matchVariable)) + ) + ); + } + } + return match; + } + + /// + /// ldobj(ldflda ItemN(ldloc(a) container)) + /// The returned index is zero-based; Rest chains of long tuples are flattened. + /// Non-escaping element reads may have been rewritten from ldloca to ldloc, + /// so both load kinds are accepted. + /// + static bool MatchTupleElementRead(ILInstruction inst, [NotNullWhen(true)] out ILVariable? container, [NotNullWhen(true)] out IType? containerType, out int index) + { + container = null; + containerType = null; + index = -1; + if (!(inst is LdObj ldobj && ldobj.Target is LdFlda ldflda)) + return false; + if (ldobj.UnalignedPrefix != 0 || ldobj.IsVolatile) + return false; + if (!TupleTransform.MatchTupleFieldAccess(ldflda, out containerType, out var target, out int position)) + return false; + // Item fields are one-based, we use zero-based indexing. + index = position - 1; + return MatchLdLocOrLdLoca(target, out container); + } + + /// + /// stloc temp(ldobj(ldflda ItemN(ldloc(a) container))) + /// The store of a nested tuple designation temporary. + /// + static bool MatchTupleElementStore(ILInstruction? inst, [NotNullWhen(true)] out ILVariable? temp, [NotNullWhen(true)] out ILVariable? container, [NotNullWhen(true)] out IType? containerType, out int index) + { + if (inst is StLoc store && MatchTupleElementRead(store.Value, out container, out containerType, out index)) + { + temp = store.Variable; + return true; + } + temp = null; + container = null; + containerType = null; + index = -1; + return false; + } + + /// + /// ldloc variable | ldloca variable + /// + static bool MatchLdLocOrLdLoca(ILInstruction inst, [NotNullWhen(true)] out ILVariable? variable) + { + return inst.MatchLdLoc(out variable) || inst.MatchLdLoca(out variable); + } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/TupleType.cs b/ICSharpCode.Decompiler/TypeSystem/TupleType.cs index 0da4777f1e..7576e817d6 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TupleType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TupleType.cs @@ -115,8 +115,9 @@ public static bool IsTupleCompatible(IType type, out int tupleCardinality) case TypeKind.Tuple: tupleCardinality = ((TupleType)type).ElementTypes.Length; return true; - case TypeKind.Class: case TypeKind.Struct: + // C# requires System.ValueTuple to be a struct, so a class of that name is + // some other type that happens to share it and must not become tuple syntax. if (type.Namespace == "System" && type.Name == "ValueTuple") { int tpc = type.TypeParameterCount; @@ -147,7 +148,7 @@ public static bool IsTupleCompatible(IType type, out int tupleCardinality) public static TupleType FromUnderlyingType(ICompilation compilation, IType type) { var elementTypes = GetTupleElementTypes(type); - if (elementTypes.Length > 0) + if (!elementTypes.IsDefaultOrEmpty) { return new TupleType( compilation,