Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
Expand Down Expand Up @@ -419,6 +420,26 @@ public void LocalVariable_Nested_BothElementsNested()
Console.WriteLine(value2);
}

public void LocalVariable_Nested_StructInnerFirstElement()
{
var ((value, value2), value3) = GetSource<StructDeconstructionSource<int, string>, int>();
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value3);
}

public void LocalVariable_ElementOfElementRead_ThenDeconstruct()
{
((StructDeconstructionSource<int, string>, int), int) tuple = GetTuple<(StructDeconstructionSource<int, string>, int), int>();
(StructDeconstructionSource<int, string>, int) item = tuple.Item1;
StructDeconstructionSource<int, string> 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<MyInt?, DeconstructionSource<MyInt, StructDeconstructionSource<int, int>>>();
Expand Down Expand Up @@ -964,5 +985,43 @@ public void DeconstructTupleListForEach(List<(string, int)> tuples)
Console.WriteLine(text + ": " + num);
}
}

public async Task<int> DeconstructionAssignmentToCapturedLocals(string file)
{
int a = 0;
int b = 0;
await Task.Run(delegate {
(a, b) = GetTuple<int, int>();
});
return a + b;
}

public void NestedDesignation_RestChainedInnerTuple()
{
var (value, (value2, value3, value4, value5, value6, value7, value8, value9)) = GetTuple<int, (int, int, int, int, int, int, int, int)>();
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value3);
Console.WriteLine(value4);
Console.WriteLine(value5);
Console.WriteLine(value6);
Console.WriteLine(value7);
Console.WriteLine(value8);
Console.WriteLine(value9);
}

public bool DeconstructStructParameter(StructDeconstructionSource<int, string> point)
{
var (num2, value) = point;
Console.WriteLine(value);
return num2 >= 0;
}

public void DeconstructStructLocal()
{
StructDeconstructionSource<int, string> structSource = GetStructSource<int, string>();
var (num2, text2) = structSource;
Console.WriteLine(num2 + text2 + structSource.Dummy);
}
}
}
17 changes: 17 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ public record struct Pair<A, B>

public record struct PairWithPrimaryCtor<A, B>(A First, B Second);

public readonly record struct BoundsInfo(int Profile, Bounds Bounds);

public readonly record struct Bounds(ulong Small, ulong Large);

public record struct PrimaryCtor(int A, string B);

public record struct MultipleCtorsNoPrimaryCtor
Expand Down Expand Up @@ -447,6 +451,19 @@ public RecordCtorChain(string B)
C = 1.41;
}
}

private static BoundsInfo GetInfo()
{
return new BoundsInfo(1, new Bounds(2uL, 3uL));
}

public static void NestedDeconstruction()
{
var (value, (value2, value3)) = GetInfo();
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value3);
}
}

internal class RecordsWithCustomSynthesizedMembers
Expand Down
76 changes: 74 additions & 2 deletions ICSharpCode.Decompiler/IL/Transforms/DeconstructionTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ bool TransformDeconstruction(Block block, int pos)
// 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))
{
Expand Down Expand Up @@ -431,7 +442,19 @@ sealed class DeconstructionCall
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;
Expand Down Expand Up @@ -460,6 +483,46 @@ static void CollectLeaves(DeconstructionCall call, List<ILVariable> leaves)
}
}

/// <summary>
/// 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,
/// <paramref name="copiedValue"/> is the deconstructed value and <paramref name="call"/>
/// the matched call, so callers need not re-match either.
/// </summary>
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;
}

/// <summary>
/// call(virt) Deconstruct(target, ldloca out0, ldloca out1, ...)
/// where every out-argument is a single-use temporary.
Expand Down Expand Up @@ -668,7 +731,16 @@ static bool AllUsesAreTupleElementReads(ILVariable temp)
{
foreach (var use in temp.AddressInstructions.Concat<ILInstruction>(temp.LoadInstructions))
{
if (!(use.Parent is LdFlda elementAccess && elementAccess.Parent is LdObj))
// Walk the ldflda chain up to the reading ldobj: element 8+ of a long tuple
// is accessed through the Rest field, i.e. through more than one ldflda.
// Whether each read is really a tuple element access (and consumed by the
// pattern) is verified by MatchTupleElementRead and the escape check.
ILInstruction? node = use.Parent;
if (node is not LdFlda)
return false;
while (node is LdFlda ldflda)
node = ldflda.Parent;
if (node is not LdObj)
return false;
}
return true;
Expand Down
38 changes: 29 additions & 9 deletions ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,37 @@ internal static void AddressOfLdLocToLdLoca(LdObj inst, ILTransformContext conte
temp = ldfldaTarget;
range = range.Concat(temp.ILRanges);
}
if (temp.MatchAddressOf(out var addressOfTarget, out _) && addressOfTarget.MatchLdLoc(out var v))
if (!temp.MatchAddressOf(out var addressOfTarget, out var addressOfType))
return;
ILInstruction replacement;
switch (addressOfTarget)
{
context.Step($"ldobj(...(addressof(ldloca {v.Name}))) => ldobj(...(ldloca {v.Name}))", inst);
var replacement = new LdLoca(v).WithILRange(addressOfTarget);
foreach (var r in range)
{
replacement = replacement.WithILRange(r);
}
temp.ReplaceWith(replacement);
context.EndStep(replacement);
case LdLoc { Variable: var v }:
context.Step($"ldobj(...(addressof(ldloca {v.Name}))) => ldobj(...(ldloca {v.Name}))", inst);
replacement = new LdLoca(v).WithILRange(addressOfTarget);
break;
// The enclosing ldobj only reads through the temporary the addressof creates, so
// the copy can be elided and the read go directly through the inner address.
// This shape arises when a struct field chain is read by value (ldfld, then a
// field of the loaded value), e.g. reading element 8+ of a long tuple via Rest.
// The type check keeps the elision on that shape: re-rooting a read of an
// incompatible type onto the inner address would read memory behind it that the
// copy never covered.
case LdObj { UnalignedPrefix: 0, IsVolatile: false } innerLdObj
when TypeUtils.IsCompatibleTypeForMemoryAccess(innerLdObj.Type, addressOfType):
context.Step("ldobj(...(addressof(ldobj(addr)))) => ldobj(...(addr))", inst);
replacement = innerLdObj.Target;
range = range.Concat(addressOfTarget.ILRanges);
break;
default:
return;
}
foreach (var r in range)
{
replacement = replacement.WithILRange(r);
}
temp.ReplaceWith(replacement);
context.EndStep(replacement);
}

protected internal override void VisitCall(Call inst)
Expand Down
Loading