diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs index 1fc53d916..21818fe18 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { @@ -419,6 +420,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Console.WriteLine(value2); } + public void LocalVariable_Nested_StructInnerFirstElement() + { + var ((value, value2), value3) = GetSource, int>(); + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(value3); + } + + public void LocalVariable_ElementOfElementRead_ThenDeconstruct() + { + ((StructDeconstructionSource, int), int) tuple = GetTuple<(StructDeconstructionSource, int), int>(); + (StructDeconstructionSource, int) item = tuple.Item1; + StructDeconstructionSource item2 = item.Item1; + var (value, value2) = item2; + Console.WriteLine(value); + Console.WriteLine(value2); + Console.WriteLine(item.Item2); + Console.WriteLine(tuple.Item2); + } + public void LocalVariable_Nested_Depth3() { var (myInt3, (myInt4, (value, value2))) = GetSource>>(); @@ -964,5 +985,29 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Console.WriteLine(text + ": " + num); } } + + public async Task DeconstructionAssignmentToCapturedLocals(string file) + { + int a = 0; + int b = 0; + await Task.Run(delegate { + (a, b) = GetTuple(); + }); + return a + b; + } + + public bool DeconstructStructParameter(StructDeconstructionSource point) + { + var (num2, value) = point; + Console.WriteLine(value); + return num2 >= 0; + } + + public void DeconstructStructLocal() + { + StructDeconstructionSource structSource = GetStructSource(); + var (num2, text2) = structSource; + Console.WriteLine(num2 + text2 + structSource.Dummy); + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs index 63324af82..f3e061505 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs @@ -129,6 +129,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms // exists (see the guard for the precision guarantees). if (IsConsumableByEnclosingDeconstruction(block, pos)) return false; + // Same idea for the value-semantics copy before a root Deconstruct call: blocks are + // processed back to front, so defer the call-only match to the attempt starting at + // the copy, which can consume both (see MatchDeconstruction). Defer only when that + // attempt actually reaches its match: an attempt that is itself deferred to an + // enclosing pattern bails without consuming this position, and the back-to-front + // walk never comes back, which would lose the deconstruction at both positions. + if (pos > 0 && IsRootDeconstructionCopy(block, pos - 1, out _, out _) + && !IsConsumableByEnclosingDeconstruction(block, pos - 1)) + { + return false; + } if (!MatchDeconstructionSequence(block, startPos, out pos, out var rootCall, out var rootTestedOperand, out var conversionStLocs, out var delayedActions)) { @@ -431,7 +442,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms void MatchDeconstruction(Block block, ref int pos, out DeconstructionCall? rootCall, out ILInstruction? testedOperand) { - rootCall = MatchDeconstructionCall(block.Instructions[pos], out testedOperand); + // Deconstruction assignment has value semantics, so Roslyn copies the deconstructed + // value into a temporary and calls Deconstruct on that. When the value is a call + // result, inlining already folds the temporary away; when it is a local or parameter, + // the copy survives to here. Consume it into the pattern: rendering the copied value + // as the deconstruction target recompiles to the identical temporary. + rootCall = null; + testedOperand = null; + if (IsRootDeconstructionCopy(block, pos, out var copiedValue, out rootCall)) + { + testedOperand = copiedValue; + pos++; + } + rootCall ??= MatchDeconstructionCall(block.Instructions[pos], out testedOperand); if (rootCall == null) return; rootedInDeconstructCall = true; @@ -460,6 +483,46 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// stloc copy(value) at pos + /// call Deconstruct(ldloc(a) copy, ...) a root Deconstruct call on the copy + /// where the copy has no other use: the value-semantics temporary Roslyn emits for a + /// deconstruction whose right-hand side is not already a temporary. On success, + /// is the deconstructed value and + /// the matched call, so callers need not re-match either. + /// + bool IsRootDeconstructionCopy(Block block, int pos, out ILInstruction? copiedValue, + out DeconstructionCall? call) + { + copiedValue = null; + call = null; + if (pos + 1 >= block.Instructions.Count) + return false; + if (!block.Instructions[pos].MatchStLoc(out var copy, out var value)) + return false; + if (copy.Kind is not (VariableKind.Local or VariableKind.StackSlot)) + return false; + // A byref temporary is not a copy: Deconstruct called through it acts on the original, + // which is not what a value-semantics deconstruction of the referenced expression does. + if (copy.StackType == StackType.Ref) + return false; + if (!(copy.StoreCount == 1 && copy.LoadCount + copy.AddressCount == 1)) + return false; + // The defensive copy of a struct element of an enclosing Deconstruct call has this + // exact shape. It belongs to the enclosing call's nested designation, so consuming it + // here would commit the inner call on its own and break the designation for good. + if (TryFindEnclosingDeconstructionCall(block, pos + 1, out _)) + return false; + var matchedCall = MatchDeconstructionCall(block.Instructions[pos + 1], out var testedOperand); + if (matchedCall == null) + return false; + if (!MatchLdLocOrLdLoca(testedOperand!, out var receiver) || receiver != copy) + return false; + copiedValue = value; + call = matchedCall; + return true; + } + /// /// call(virt) Deconstruct(target, ldloca out0, ldloca out1, ...) /// where every out-argument is a single-use temporary.