Browse Source

Keep the Deconstruct call explicit when the element type hides it

A nested designation rebinds Deconstruct on the element's static type when
the output is recompiled, while the explicit call it replaces is bound at
the call site. Where a derived element type declares a Deconstruct of the
same arity as the called method, and the source deconstructs through a
base-typed view, the two bindings differ, so the sugared output calls the
wrong method - a divergence the runtime fixture demonstrates on optimized
builds, where copy propagation elides the view.

Nesting is therefore only applied when the method the call binds to is the
one a designation would rebind to; otherwise the call stays explicit, where
its receiver cast preserves the binding.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/3869/merge
Siegfried Pammer 1 month ago committed by Siegfried Pammer
parent
commit
8a455f44a5
  1. 46
      ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs
  2. 42
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs
  3. 35
      ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs

46
ICSharpCode.Decompiler.Tests/TestCases/Correctness/DeconstructionTests.cs

@ -168,6 +168,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness @@ -168,6 +168,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
NestedDeconstruction_Conversions_AfterAllDeconstructCalls();
NestedDeconstruction_TypedDeclaration_Conversions(new NestedOuter { Value = 5 });
NestedDeconstruction_DiscardWithSideEffectTargets();
NestedDeconstruction_HiddenDeconstructMethod(default(HidingOuter));
NestedDeconstruction_SystemTupleSource(Tuple.Create(8, new NestedInner { Value = 4 }));
NestedDeconstruction_CheckedConversions(new NestedOuter { Value = 9 });
NestedDeconstruction_GenericConstraintSource(new ConstrainedSource { Value = 11 });
@ -303,6 +304,51 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness @@ -303,6 +304,51 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
(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()");

42
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs

@ -22,6 +22,14 @@ using System.Runtime.InteropServices; @@ -22,6 +22,14 @@ using System.Runtime.InteropServices;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
public class DeconstructionBase
{
}
public class DeconstructionDerived : DeconstructionBase
{
}
public static class DeconstructionExt
{
public static void Deconstruct<K, V>(this KeyValuePair<K, V> pair, out K key, out V value)
@ -35,6 +43,27 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -35,6 +43,27 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
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
@ -529,6 +558,19 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -529,6 +558,19 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
// 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;

35
ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs

@ -477,6 +477,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -477,6 +477,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
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;
@ -488,6 +496,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -488,6 +496,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
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);
}
}
struct ConversionInfo

Loading…
Cancel
Save