Browse Source

Fix #1388, #1928: decompile await on dynamic expressions

Awaiting a dynamic value lowers GetAwaiter/IsCompleted/GetResult to
dynamic callsites, which async decompilation could not recognize:
await detection ran before DynamicCallSiteTransform, so the await was
emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).

AnalyzeStateMachine now collapses the awaiter callsites per block, folds
the runtime ICriticalNotifyCompletion branch the compiler emits for an
awaiter not statically known to implement it into the canonical single
call, and re-joins the branch chains the collapse leaves so each dynamic
await sits in one block. DetectAwaitPattern matches the dynamic
GetAwaiter/IsCompleted/GetResult shape and emits `await expr`; a
synthesized dynamic GetResult method gives the await and its local the
dynamic type. DynamicCallSiteTransform also follows callsite targets
spilled into state-machine locals, so an awaited value flowing into a
dynamic callsite (e.g. d.Result = await ..., #1928) decompiles too.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3878/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
c52a0f29f6
  1. 6
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 191
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs
  3. 238
      ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
  4. 2
      ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs
  5. 140
      ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs
  6. 21
      ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs

6
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -488,6 +488,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -488,6 +488,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task DynamicAwait([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task ExpressionTrees([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{

191
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs

@ -0,0 +1,191 @@ @@ -0,0 +1,191 @@
using System;
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class DynamicAwait
{
private static Task DoAsync(object o)
{
return Task.CompletedTask;
}
private static dynamic GetDynamic()
{
return null;
}
private static Task<int> GetIntAsync(object o)
{
return Task.FromResult(0);
}
private static Task<object> GetObjAsync()
{
return Task.FromResult<object>(null);
}
// --- dynamic operations around a normal (real-Task) await ---
private static async Task SetMemberBeforeAwait(dynamic d)
{
d.Before = 1;
await DoAsync(null);
}
private static async Task SetMemberAfterAwait(dynamic d)
{
await DoAsync(null);
d.After = 1;
}
private static async Task SetMemberAroundAwait(dynamic d)
{
d.Before = 1;
await DoAsync(null);
d.After = 2;
}
private static async Task InvokeBeforeAwait(dynamic d)
{
d.Before();
await DoAsync(null);
}
private static async Task InvokeAfterAwait(dynamic d)
{
await DoAsync(null);
d.After();
}
private static async Task DynamicInLoopWithAwait(dynamic d)
{
for (int i = 0; i < 10; i++)
{
d.Add(i);
await DoAsync(null);
}
}
private static async Task DynamicWhileConditionWithAwait(dynamic d)
{
while ((bool)d.HasNext)
{
await DoAsync(null);
}
}
private static async Task TwoAwaitsWithDynamicBetween(dynamic d)
{
await DoAsync(null);
d.Middle = 1;
await DoAsync(null);
}
private static async Task DynamicConditionGuardingAwait(dynamic d)
{
if ((bool)d.Flag)
{
await DoAsync(null);
}
}
private static async Task DynamicAwaitInTryCatch(dynamic d)
{
try
{
d.Before = 1;
await DoAsync(null);
}
catch
{
}
}
// --- dynamic feeding the awaited expression (awaiter stays a real Task) ---
private static async Task DynamicConvertedArgumentToAwaitedCall(dynamic d)
{
await DoAsync((string)d.Value);
}
private static async Task AwaitTaskFromDynamicCast(dynamic d)
{
await (Task)d.GetTask();
}
private static async Task<int> AwaitIntTaskFromDynamicCast(dynamic d)
{
return await (Task<int>)d.GetIntTask();
}
// --- awaited result feeding a dynamic operation ---
private static async Task StoreAwaitResultInDynamicMember(dynamic d)
{
d.Result = await GetIntAsync(null);
}
private static async Task PassAwaitResultToDynamicInvoke(dynamic d)
{
d.Consume(await GetObjAsync());
}
private static async Task DynamicIndexerWithAwaitedIndex(dynamic d)
{
d[await GetIntAsync(null)] = 1;
}
private static async Task<object> DynamicBinaryOpWithAwaitedOperand(dynamic d)
{
return d + await GetIntAsync(null);
}
private static async Task<int> DynamicConvertOfAwaitedResult()
{
return (int)(dynamic)(await GetObjAsync());
}
// --- real-world shapes from the referenced issues ---
// awaited dynamic stored in a dynamic local, then used (#1388)
private static async Task<dynamic> AwaitDynamicIntoUsedLocal()
{
dynamic val = await GetDynamic();
val.UseMe();
return val;
}
// dynamic-dispatched awaited call with a dynamic-converted result (#1928)
private static async Task<string> AwaitDynamicConvertResult(dynamic d)
{
return (string)(await d.RunAsync());
}
// --- fully dynamic awaiter (the awaitable itself is dynamic) ---
private static async Task AwaitDynamicValue(dynamic d)
{
await d;
}
private static async Task AwaitDynamicMethodResult(dynamic d)
{
await d.RunAsync();
}
private static async Task AwaitDynamicLocalWithStatementBefore()
{
// Optimized builds drop the local's debug name, so the decompiler regenerates one from the type.
#if OPT
dynamic dynamic = GetDynamic();
Console.WriteLine("before await");
await dynamic;
#else
dynamic x = GetDynamic();
Console.WriteLine("before await");
await x;
#endif
}
}
}

238
ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs

@ -28,6 +28,7 @@ using ICSharpCode.Decompiler.DebugInfo; @@ -28,6 +28,7 @@ using ICSharpCode.Decompiler.DebugInfo;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.Decompiler.Util;
namespace ICSharpCode.Decompiler.IL.ControlFlow
@ -1308,6 +1309,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1308,6 +1309,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
smallestAwaiterVarIndex = int.MaxValue;
foreach (var container in function.Descendants.OfType<BlockContainer>())
{
// Fold the runtime ICriticalNotifyCompletion type-check that the compiler emits when the
// awaiter's static type is not known to implement it (dynamic awaiters, some generic awaiters)
// into the canonical single-call form that AnalyzeAwaitBlock recognizes.
foreach (var block in container.Blocks)
NormalizeAwaitOnCompletedDualBranch(block);
// Use a separate state range analysis per container.
var sra = new StateRangeAnalysis(StateRangeAnalysisMode.AsyncMoveNext, stateField, cachedStateVar);
sra.CancellationToken = context.CancellationToken;
@ -1318,6 +1325,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1318,6 +1325,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
foreach (var block in container.Blocks)
{
context.CancellationToken.ThrowIfCancellationRequested();
DynamicCallSiteTransform.RunOnBasicBlock(block, context);
if (block.Instructions.Last() is Leave leave && moveNextLeaves.Contains(leave))
{
// This is likely an 'await' block
@ -1381,6 +1389,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1381,6 +1389,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
});
}
container.SortBlocks(deleteUnreachableBlocks: true);
// Collapsing the dynamic awaiter callsites left each await's GetAwaiter/IsCompleted/GetResult spread
// across branch-joined blocks; the SortBlocks above removed the unreachable init blocks, so re-join
// the chains into single blocks for DetectAwaitPattern.
CoalesceDynamicAwaiterBlocks(container, context);
container.SortBlocks(deleteUnreachableBlocks: true);
}
context.StepEndGroup();
}
@ -1498,6 +1511,107 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1498,6 +1511,107 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
}
/// <summary>
/// When the awaiter's static type is not known to implement ICriticalNotifyCompletion (dynamic
/// awaiters, and some generic awaiters), the C# compiler picks between AwaitOnCompleted and
/// AwaitUnsafeOnCompleted at runtime:
/// stloc criticalVar(isinst ICriticalNotifyCompletion(ldloc awaiterVar))
/// [stloc smVar(ldloc this)] // class state machine only
/// if (comp.o(ldloc criticalVar != ldnull)) br unsafeOnCompletedBlock
/// br onCompletedBlock
/// unsafeOnCompletedBlock:
/// call AwaitUnsafeOnCompleted(ldflda builder(this), ldloca criticalVar, ldloca smVar); br mergeBlock
/// onCompletedBlock:
/// stloc notifyVar(castclass INotifyCompletion(ldloc awaiterVar))
/// call AwaitOnCompleted(ldflda builder(this), ldloca notifyVar, ldloca smVar); br mergeBlock
/// mergeBlock:
/// leave
/// Both branches await the same awaiter, so this folds the whole diamond into the single-call form
/// AnalyzeAwaitBlock recognizes, awaiting the awaiter variable directly.
/// </summary>
bool NormalizeAwaitOnCompletedDualBranch(Block block)
{
int count = block.Instructions.Count;
if (count < 3)
return false;
if (!block.Instructions[count - 1].MatchBranch(out var onCompletedBlock))
return false;
if (!block.Instructions[count - 2].MatchIfInstruction(out var condition, out var trueBranch))
return false;
if (!trueBranch.MatchBranch(out var unsafeOnCompletedBlock))
return false;
if (!condition.MatchCompNotEqualsNull(out var criticalTest) || !criticalTest.MatchLdLoc(out var criticalVar))
return false;
int isInstPos = count - 3;
ILVariable smVar = null;
if (isInstPos >= 0 && block.Instructions[isInstPos].MatchStLoc(out var smCandidate, out var smValue) && smValue.MatchLdThis())
{
smVar = smCandidate;
isInstPos--;
}
if (isInstPos < 0 || !block.Instructions[isInstPos].MatchStLoc(criticalVar, out var criticalValue))
return false;
if (!(criticalValue is IsInst isInst)
|| isInst.Type.FullName != "System.Runtime.CompilerServices.ICriticalNotifyCompletion"
|| !isInst.Argument.MatchLdLoc(out var awaiterVar))
{
return false;
}
bool MatchStateMachineArg(ILInstruction arg) => smVar != null ? (arg.MatchLdLoca(out var v) && v == smVar) : arg.MatchLdThis();
// unsafeOnCompletedBlock: call AwaitUnsafeOnCompleted(builder, ldloca criticalVar, smArg); br mergeBlock
if (unsafeOnCompletedBlock.Instructions.Count != 2)
return false;
if (!MatchCall(unsafeOnCompletedBlock.Instructions[0], "AwaitUnsafeOnCompleted", out var unsafeArgs)
|| unsafeArgs.Count != 3 || !IsBuilderFieldOnThis(unsafeArgs[0])
|| !unsafeArgs[1].MatchLdLoca(out var unsafeAwaiter) || unsafeAwaiter != criticalVar
|| !MatchStateMachineArg(unsafeArgs[2]))
{
return false;
}
if (!unsafeOnCompletedBlock.Instructions[1].MatchBranch(out var mergeBlock))
return false;
// onCompletedBlock: stloc notifyVar(castclass INotifyCompletion(ldloc awaiterVar)); call AwaitOnCompleted(builder, ldloca notifyVar, smArg); br mergeBlock
if (onCompletedBlock.Instructions.Count != 3)
return false;
if (!onCompletedBlock.Instructions[0].MatchStLoc(out var notifyVar, out var notifyValue)
|| !(notifyValue is CastClass castClass)
|| castClass.Type.FullName != "System.Runtime.CompilerServices.INotifyCompletion"
|| !castClass.Argument.MatchLdLoc(out var castAwaiter) || castAwaiter != awaiterVar)
{
return false;
}
if (!MatchCall(onCompletedBlock.Instructions[1], "AwaitOnCompleted", out var safeArgs)
|| safeArgs.Count != 3 || !IsBuilderFieldOnThis(safeArgs[0])
|| !safeArgs[1].MatchLdLoca(out var safeAwaiter) || safeAwaiter != notifyVar
|| !MatchStateMachineArg(safeArgs[2]))
{
return false;
}
if (!onCompletedBlock.Instructions[2].MatchBranch(out var mergeBlock2) || mergeBlock2 != mergeBlock)
return false;
if (mergeBlock.Instructions.Count != 1 || !(mergeBlock.Instructions[0] is Leave mergeLeave))
return false;
// Rewrite `block` into the canonical single-call form.
context.Step("Normalize dynamic AwaitOnCompleted", block);
var awaitCall = (CallInstruction)unsafeOnCompletedBlock.Instructions[0];
unsafeOnCompletedBlock.Instructions.RemoveAt(0);
awaitCall.Arguments[1].ReplaceWith(new LdLoca(awaiterVar));
// Remove the isinst test, the if, and the branch; keep the state/awaiter-field stores (and smVar store).
block.Instructions.RemoveRange(count - 2, 2); // if + br
block.Instructions.RemoveAt(isInstPos); // isinst store
block.Instructions.Add(awaitCall);
var newLeave = (Leave)mergeLeave.Clone();
moveNextLeaves.Add(newLeave);
block.Instructions.Add(newLeave);
return true;
}
bool AnalyzeAwaitBlock(Block block, out ILVariable awaiter, out IField awaiterField, out int state, out int yieldOffset)
{
awaiter = null;
@ -1704,12 +1818,29 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1704,12 +1818,29 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
if (!(block.Instructions[block.Instructions.Count - 3] is StLoc stLocAwaiter))
return;
ILVariable awaiterVar = stLocAwaiter.Variable;
if (!(stLocAwaiter.Value is CallInstruction getAwaiterCall))
return;
if (!(getAwaiterCall.Method.Name == "GetAwaiter" && (!getAwaiterCall.Method.IsStatic || getAwaiterCall.Method.IsExtensionMethod)))
return;
if (getAwaiterCall.Arguments.Count != 1)
ILInstruction awaitedValue;
IMethod getAwaiterMethod;
bool isDynamicAwait = false;
if (stLocAwaiter.Value is CallInstruction getAwaiterCall
&& getAwaiterCall.Method.Name == "GetAwaiter"
&& (!getAwaiterCall.Method.IsStatic || getAwaiterCall.Method.IsExtensionMethod)
&& getAwaiterCall.Arguments.Count == 1)
{
awaitedValue = getAwaiterCall.Arguments[0];
getAwaiterMethod = getAwaiterCall.Method;
}
else if (stLocAwaiter.Value is DynamicInvokeMemberInstruction dynGetAwaiter
&& dynGetAwaiter.Name == "GetAwaiter" && dynGetAwaiter.Arguments.Count == 1)
{
// awaiting a dynamic value: GetAwaiter/IsCompleted/GetResult are dynamic callsites
awaitedValue = dynGetAwaiter.Arguments[0];
getAwaiterMethod = CreateDynamicAwaiterMethod(context, "GetAwaiter");
isDynamicAwait = true;
}
else
{
return;
}
// if (call get_IsCompleted(ldloca awaiterVar)) br completedBlock
if (!block.Instructions[block.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst))
return;
@ -1724,11 +1855,25 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1724,11 +1855,25 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
condition = negatedCondition;
ExtensionMethods.Swap(ref completedBlock, ref awaitBlock);
}
// continue matching call get_IsCompleted(ldloca awaiterVar)
if (!MatchCall(condition, "get_IsCompleted", out var isCompletedArgs) || isCompletedArgs.Count != 1)
return;
if (!UnwrapConvUnknown(isCompletedArgs[0]).MatchLdLocRef(awaiterVar))
return;
if (isDynamicAwait)
{
// if (dynamic.convert bool(dynamic.getmember "IsCompleted"(ldloc awaiterVar)))
ILInstruction isCompletedTest = condition;
if (isCompletedTest is DynamicConvertInstruction dynConv && dynConv.Type.IsKnownType(KnownTypeCode.Boolean))
isCompletedTest = dynConv.Argument;
if (!(isCompletedTest is DynamicGetMemberInstruction dynIsCompleted && dynIsCompleted.Name == "IsCompleted"))
return;
if (!dynIsCompleted.Target.MatchLdLoc(awaiterVar))
return;
}
else
{
// continue matching call get_IsCompleted(ldloca awaiterVar)
if (!MatchCall(condition, "get_IsCompleted", out var isCompletedArgs) || isCompletedArgs.Count != 1)
return;
if (!UnwrapConvUnknown(isCompletedArgs[0]).MatchLdLocRef(awaiterVar))
return;
}
// Check awaitBlock and resumeBlock:
if (!awaitBlocks.TryGetValue(awaitBlock, out var awaitBlockData))
return;
@ -1740,22 +1885,39 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1740,22 +1885,39 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return;
// Check completedBlock. The first instruction involves the GetResult call, but it might have
// been inlined into another instruction.
var getResultCall = ILInlining.FindFirstInlinedCall(completedBlock.Instructions[0]);
if (getResultCall == null)
return;
if (!MatchCall(getResultCall, "GetResult", out var getResultArgs) || getResultArgs.Count != 1)
return;
if (!UnwrapConvUnknown(getResultArgs[0]).MatchLdLocRef(awaiterVar))
return;
ILInstruction getResultInst;
IMethod getResultMethod;
if (isDynamicAwait)
{
// dynamic.invokemember "GetResult"(ldloc awaiterVar), possibly inlined into another instruction
getResultInst = completedBlock.Instructions[0].Descendants.Prepend(completedBlock.Instructions[0])
.OfType<DynamicInvokeMemberInstruction>()
.FirstOrDefault(d => d.Name == "GetResult" && d.Arguments.Count == 1 && d.Arguments[0].MatchLdLoc(awaiterVar));
if (getResultInst == null)
return;
getResultMethod = CreateDynamicAwaiterMethod(context, "GetResult");
}
else
{
var getResultCall = ILInlining.FindFirstInlinedCall(completedBlock.Instructions[0]);
if (getResultCall == null)
return;
if (!MatchCall(getResultCall, "GetResult", out var getResultArgs) || getResultArgs.Count != 1)
return;
if (!UnwrapConvUnknown(getResultArgs[0]).MatchLdLocRef(awaiterVar))
return;
getResultInst = getResultCall;
getResultMethod = getResultCall.Method;
}
// All checks successful, let's transform.
context.Step("Transform await pattern", block);
block.Instructions.RemoveAt(block.Instructions.Count - 3); // remove getAwaiter call
block.Instructions.RemoveAt(block.Instructions.Count - 2); // remove if (isCompleted)
((Branch)block.Instructions.Last()).TargetBlock = completedBlock; // instead, directly jump to completed block
Await awaitInst = new Await(UnwrapConvUnknown(getAwaiterCall.Arguments.Single()));
awaitInst.GetResultMethod = getResultCall.Method;
awaitInst.GetAwaiterMethod = getAwaiterCall.Method;
getResultCall.ReplaceWith(awaitInst);
Await awaitInst = new Await(UnwrapConvUnknown(awaitedValue));
awaitInst.GetResultMethod = getResultMethod;
awaitInst.GetAwaiterMethod = getAwaiterMethod;
getResultInst.ReplaceWith(awaitInst);
// Remove useless reset of awaiterVar.
if (completedBlock.Instructions.ElementAtOrDefault(1) is StObj stobj)
@ -1776,6 +1938,40 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -1776,6 +1938,40 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return inst;
}
/// <summary>
/// Synthesizes a GetAwaiter/GetResult method on the dynamic type, so a dynamic await carries the
/// type 'dynamic': the Await result type derives from GetResultMethod.ReturnType, and the dynamic
/// declaring type also marks the await as dynamic for IntroduceDynamicTypeOnLocals.
/// </summary>
static IMethod CreateDynamicAwaiterMethod(ILTransformContext context, string name)
{
return new FakeMethod(context.TypeSystem, SymbolKind.Method) {
Name = name,
DeclaringType = SpecialType.Dynamic,
ReturnType = SpecialType.Dynamic,
};
}
/// <summary>
/// Collapsing the dynamic GetAwaiter/IsCompleted/GetResult callsites leaves each dynamic await spread
/// across unconditional-branch-joined blocks. Re-join those chains so each dynamic await ends up in a
/// single block for DetectAwaitPattern. Await blocks are skipped so their branch to the resume block
/// survives; the unreachable callsite-init blocks must already be gone, otherwise their dangling branch
/// would keep the "after init" block from having the single predecessor CombineBlockWithNextBlock needs.
/// </summary>
void CoalesceDynamicAwaiterBlocks(BlockContainer container, ILTransformContext context)
{
foreach (var block in container.Blocks)
{
if (block.Instructions.Count == 0 || awaitBlocks.ContainsKey(block))
continue;
while (ControlFlowSimplification.CombineBlockWithNextBlock(container, block, context))
{
// keep pulling in trivial branch targets until this block can grow no further
}
}
}
bool CheckAwaitBlock(Block block, out Block resumeBlock, out IField stackField)
{
// awaitBlock:

2
ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs

@ -241,7 +241,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -241,7 +241,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return true;
}
static bool CombineBlockWithNextBlock(BlockContainer container, Block block, ILTransformContext context)
internal static bool CombineBlockWithNextBlock(BlockContainer container, Block block, ILTransformContext context)
{
Debug.Assert(container == block.Parent);
// Ensure the block will stay a basic block -- we don't want extended basic blocks prior to LoopDetection.

140
ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs

@ -45,53 +45,43 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -45,53 +45,43 @@ namespace ICSharpCode.Decompiler.IL.Transforms
this.context = context;
Dictionary<IField, CallSiteInfo> callsites = new Dictionary<IField, CallSiteInfo>();
HashSet<BlockContainer> modifiedContainers = new HashSet<BlockContainer>();
foreach (var block in function.Descendants.OfType<Block>())
{
if (block.Instructions.Count < 2)
continue;
// Check if, we deal with a callsite cache field null check:
// if (comp(ldsfld <>p__3 == ldnull)) br IL_000c
// br IL_002b
if (!(block.Instructions.SecondToLastOrDefault() is IfInstruction ifInst))
continue;
if (!(block.Instructions.LastOrDefault() is Branch branchAfterInit))
continue;
if (!MatchCallSiteCacheNullCheck(ifInst.Condition, out var callSiteCacheField, out var callSiteDelegate, out bool invertBranches))
continue;
if (!ifInst.TrueInst.MatchBranch(out var trueBlock))
continue;
Block callSiteInitBlock, targetBlockAfterInit;
if (invertBranches)
{
callSiteInitBlock = branchAfterInit.TargetBlock;
targetBlockAfterInit = trueBlock;
}
else
{
callSiteInitBlock = trueBlock;
targetBlockAfterInit = branchAfterInit.TargetBlock;
}
if (!ScanCallSiteInitBlock(callSiteInitBlock, callSiteCacheField, callSiteDelegate, out var callSiteInfo, out var blockAfterInit))
continue;
if (targetBlockAfterInit != blockAfterInit)
continue;
callSiteInfo.DelegateType = callSiteDelegate;
callSiteInfo.ConditionalJumpToInit = ifInst;
callSiteInfo.Inverted = invertBranches;
callSiteInfo.BranchAfterInit = branchAfterInit;
callsites.Add(callSiteCacheField, callSiteInfo);
FindDynamicCallSitesInBlock(block, callsites, context);
}
var storesToRemove = new List<StLoc>();
var modifiedContainers = TransformCallSites((BlockContainer)function.Body, callsites, context);
foreach (var container in modifiedContainers)
container.SortBlocks(deleteUnreachableBlocks: true);
}
internal static void RunOnBasicBlock(Block block, ILTransformContext context)
{
if (!context.Settings.Dynamic)
return;
Dictionary<IField, CallSiteInfo> callsites = new Dictionary<IField, CallSiteInfo>();
FindDynamicCallSitesInBlock(block, callsites, context);
// Deleting the now-unreachable callsite-init blocks is deferred to the SortBlocks call
// at the end of AsyncAwaitDecompiler.AnalyzeStateMachine: this runs while that method is
// iterating over the container's blocks, so removing blocks here would corrupt the loop.
TransformCallSites((BlockContainer)block.Parent, callsites, context);
}
foreach (var invokeCall in function.Descendants.OfType<CallVirt>())
private static HashSet<BlockContainer> TransformCallSites(BlockContainer parent, Dictionary<IField, CallSiteInfo> callsites,
ILTransformContext context)
{
List<StLoc> storesToRemove = new();
HashSet<BlockContainer> modifiedContainers = new();
foreach (var invokeCall in parent.Descendants.OfType<CallVirt>())
{
if (invokeCall.Method.DeclaringType.Kind != TypeKind.Delegate || invokeCall.Method.Name != "Invoke" || invokeCall.Arguments.Count == 0)
continue;
var firstArgument = invokeCall.Arguments[0];
if (firstArgument.MatchLdLoc(out var stackSlot) && stackSlot.Kind == VariableKind.StackSlot && stackSlot.IsSingleDefinition)
if (firstArgument.MatchLdLoc(out var stackSlot) && IsSingleDefinitionTemporary(stackSlot))
{
firstArgument = ((StLoc)stackSlot.StoreInstructions[0]).Value;
}
@ -120,7 +110,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -120,7 +110,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
foreach (var arg in deadArguments)
{
if (arg.MatchLdLoc(out var temporary) && temporary.Kind == VariableKind.StackSlot && temporary.IsSingleDefinition && temporary.LoadCount == 0)
if (arg.MatchLdLoc(out var temporary) && IsSingleDefinitionTemporary(temporary) && temporary.LoadCount == 0)
{
StLoc stLoc = (StLoc)temporary.StoreInstructions[0];
if (stLoc.Parent is Block storeParentBlock)
@ -142,11 +132,61 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -142,11 +132,61 @@ namespace ICSharpCode.Decompiler.IL.Transforms
parentBlock.Instructions.RemoveAt(inst.ChildIndex);
}
foreach (var container in modifiedContainers)
container.SortBlocks(deleteUnreachableBlocks: true);
return modifiedContainers;
}
/// <summary>
/// A compiler-generated temporary the callsite invoke may load its target/cache through: a single-def
/// stack slot, or a single-def local spilled from a state-machine field. The latter arises when async
/// state-machine hoisting spills the callsite target/receiver across an await (the await appears in the
/// invoke's argument list). Ordinary user locals are excluded via the state-machine-field requirement.
/// </summary>
static bool IsSingleDefinitionTemporary(ILVariable variable)
{
if (!variable.IsSingleDefinition)
return false;
return variable.Kind == VariableKind.StackSlot
|| (variable.Kind == VariableKind.Local && variable.StateMachineField != null);
}
static void FindDynamicCallSitesInBlock(Block block, Dictionary<IField, CallSiteInfo> callsites, ILTransformContext context)
{
if (block.Instructions.Count < 2)
return;
// Check if, we deal with a callsite cache field null check:
// if (comp(ldsfld <>p__3 == ldnull)) br IL_000c
// br IL_002b
if (!(block.Instructions.SecondToLastOrDefault() is IfInstruction ifInst))
return;
if (!(block.Instructions.LastOrDefault() is Branch branchAfterInit))
return;
if (!MatchCallSiteCacheNullCheck(ifInst.Condition, out var callSiteCacheField, out var callSiteDelegate, out bool invertBranches))
return;
if (!ifInst.TrueInst.MatchBranch(out var trueBlock))
return;
Block callSiteInitBlock, targetBlockAfterInit;
if (invertBranches)
{
callSiteInitBlock = branchAfterInit.TargetBlock;
targetBlockAfterInit = trueBlock;
}
else
{
callSiteInitBlock = trueBlock;
targetBlockAfterInit = branchAfterInit.TargetBlock;
}
if (!ScanCallSiteInitBlock(callSiteInitBlock, callSiteCacheField, callSiteDelegate, out var callSiteInfo, out var blockAfterInit, context))
return;
if (targetBlockAfterInit != blockAfterInit)
return;
callSiteInfo.DelegateType = callSiteDelegate;
callSiteInfo.ConditionalJumpToInit = ifInst;
callSiteInfo.Inverted = invertBranches;
callSiteInfo.BranchAfterInit = branchAfterInit;
callsites.Add(callSiteCacheField, callSiteInfo);
}
ILInstruction MakeDynamicInstruction(CallSiteInfo callsite, CallVirt targetInvokeCall, List<ILInstruction> deadArguments)
static ILInstruction MakeDynamicInstruction(CallSiteInfo callsite, CallVirt targetInvokeCall, List<ILInstruction> deadArguments)
{
switch (callsite.Kind)
{
@ -272,7 +312,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -272,7 +312,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, IType callSiteDelegateType, out CallSiteInfo callSiteInfo, out Block blockAfterInit)
static bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, IType callSiteDelegateType, out CallSiteInfo callSiteInfo, out Block blockAfterInit, ILTransformContext context)
{
callSiteInfo = default(CallSiteInfo);
blockAfterInit = null;
@ -397,7 +437,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -397,7 +437,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!callSiteInitBlock.Instructions[4 + typeArgumentsOffset].MatchStLoc(variable, out value))
return false;
if (!ExtractArgumentInfo(value, ref callSiteInfo, 5 + typeArgumentsOffset, variable))
if (!ExtractArgumentInfo(value, ref callSiteInfo, 5 + typeArgumentsOffset, variable, context))
return false;
return true;
case "GetMember":
@ -436,7 +476,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -436,7 +476,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!callSiteInitBlock.Instructions[3].MatchStLoc(variable, out value))
return false;
if (!ExtractArgumentInfo(value, ref callSiteInfo, 4, variable))
if (!ExtractArgumentInfo(value, ref callSiteInfo, 4, variable, context))
return false;
return true;
case "GetIndex":
@ -484,7 +524,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -484,7 +524,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!callSiteInitBlock.Instructions[2].MatchStLoc(variable, out value))
return false;
if (!ExtractArgumentInfo(value, ref callSiteInfo, 3, variable))
if (!ExtractArgumentInfo(value, ref callSiteInfo, 3, variable, context))
return false;
return true;
case "UnaryOperation":
@ -523,7 +563,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -523,7 +563,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!callSiteInitBlock.Instructions[3].MatchStLoc(variable, out value))
return false;
if (!ExtractArgumentInfo(value, ref callSiteInfo, 4, variable))
if (!ExtractArgumentInfo(value, ref callSiteInfo, 4, variable, context))
return false;
return true;
default:
@ -531,7 +571,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -531,7 +571,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
bool ExtractArgumentInfo(ILInstruction value, ref CallSiteInfo callSiteInfo, int instructionOffset, ILVariable variable)
static bool ExtractArgumentInfo(ILInstruction value, ref CallSiteInfo callSiteInfo, int instructionOffset, ILVariable variable, ILTransformContext context)
{
if (!(value is NewArr newArr2 && newArr2.Type.FullName == "Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo" && newArr2.Indices.Count == 1 && newArr2.Indices[0].MatchLdcI4(out var numberOfArguments)))
return false;
@ -560,7 +600,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -560,7 +600,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true;
}
bool MatchCallSiteCacheNullCheck(ILInstruction condition, out IField callSiteCacheField, out IType callSiteDelegate, out bool invertBranches)
static bool MatchCallSiteCacheNullCheck(ILInstruction condition, out IField callSiteCacheField, out IType callSiteDelegate, out bool invertBranches)
{
callSiteCacheField = null;
callSiteDelegate = null;
@ -579,7 +619,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -579,7 +619,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true;
}
struct CallSiteInfo
internal struct CallSiteInfo
{
public bool Inverted;
public ILInstruction BranchAfterInit;
@ -596,7 +636,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -596,7 +636,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
public string MemberName;
}
enum BinderMethodKind
internal enum BinderMethodKind
{
BinaryOperation,
Convert,

21
ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs

@ -46,18 +46,25 @@ namespace ICSharpCode.Decompiler.IL @@ -46,18 +46,25 @@ namespace ICSharpCode.Decompiler.IL
continue;
foreach (var load in variable.LoadInstructions)
{
bool isDynamicUse;
if (load.Parent is DynamicInstruction dynamicInstruction)
{
var argumentInfo = dynamicInstruction.GetArgumentInfoOfChild(load.ChildIndex);
if (!argumentInfo.HasFlag(CSharpArgumentInfoFlags.UseCompileTimeType))
isDynamicUse = !argumentInfo.HasFlag(CSharpArgumentInfoFlags.UseCompileTimeType);
}
else
{
// awaiting a dynamic value: the awaited operand has type dynamic
isDynamicUse = load.Parent is Await { GetResultMethod.DeclaringType.Kind: TypeKind.Dynamic };
}
if (isDynamicUse)
{
variable.Type = SpecialType.Dynamic;
if (variable.Index.HasValue && variable.Kind == VariableKind.Local)
{
variable.Type = SpecialType.Dynamic;
if (variable.Index.HasValue && variable.Kind == VariableKind.Local)
{
dynamicVariables.Add(variable.Index.Value);
}
break;
dynamicVariables.Add(variable.Index.Value);
}
break;
}
}
}

Loading…
Cancel
Save