From 8acb91868ff7dccdba65c7ce24a3456353a75988 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 8 Jul 2026 00:09:32 +0200 Subject: [PATCH] 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 --- .../PrettyTestRunner.cs | 6 + .../TestCases/Pretty/DynamicAwait.cs | 191 ++++++++++++++ .../IL/ControlFlow/AsyncAwaitDecompiler.cs | 238 ++++++++++++++++-- .../ControlFlow/ControlFlowSimplification.cs | 2 +- .../IL/Transforms/DynamicCallSiteTransform.cs | 140 +++++++---- .../IntroduceDynamicTypeOnLocals.cs | 21 +- 6 files changed, 519 insertions(+), 79 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index f68632b758..707b1d1a61 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -488,6 +488,12 @@ public async Task DynamicTests([ValueSource(nameof(defaultOptions))] CompilerOpt 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) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs new file mode 100644 index 0000000000..3e091148d9 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs @@ -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 GetIntAsync(object o) + { + return Task.FromResult(0); + } + + private static Task GetObjAsync() + { + return Task.FromResult(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 AwaitIntTaskFromDynamicCast(dynamic d) + { + return await (Task)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 DynamicBinaryOpWithAwaitedOperand(dynamic d) + { + return d + await GetIntAsync(null); + } + + private static async Task 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 AwaitDynamicIntoUsedLocal() + { + dynamic val = await GetDynamic(); + val.UseMe(); + return val; + } + + // dynamic-dispatched awaited call with a dynamic-converted result (#1928) + private static async Task 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 + } + } +} diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs index 04baaf3584..7b4e5df76c 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -28,6 +28,7 @@ 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 @@ void AnalyzeStateMachine(ILFunction function) smallestAwaiterVarIndex = int.MaxValue; foreach (var container in function.Descendants.OfType()) { + // 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 @@ void AnalyzeStateMachine(ILFunction function) 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 @@ void AnalyzeStateMachine(ILFunction function) }); } 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 @@ bool MatchLdDisposeMode(ILInstruction inst) } } + /// + /// 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. + /// + 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 @@ void DetectAwaitPattern(Block block) 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 @@ void DetectAwaitPattern(Block block) 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 @@ void DetectAwaitPattern(Block block) 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() + .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 @@ static ILInstruction UnwrapConvUnknown(ILInstruction inst) return inst; } + /// + /// 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. + /// + static IMethod CreateDynamicAwaiterMethod(ILTransformContext context, string name) + { + return new FakeMethod(context.TypeSystem, SymbolKind.Method) { + Name = name, + DeclaringType = SpecialType.Dynamic, + ReturnType = SpecialType.Dynamic, + }; + } + + /// + /// 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. + /// + 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: diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs index af864827e5..2b99e462bb 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs @@ -241,7 +241,7 @@ bool IsBranchToReturnBlock(Branch branch) 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. diff --git a/ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs index d78cd53d20..69f858bd61 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs @@ -45,53 +45,43 @@ public void Run(ILFunction function, ILTransformContext context) this.context = context; Dictionary callsites = new Dictionary(); - HashSet modifiedContainers = new HashSet(); foreach (var block in function.Descendants.OfType()) { - 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(); + 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 callsites = new Dictionary(); + 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()) + private static HashSet TransformCallSites(BlockContainer parent, Dictionary callsites, + ILTransformContext context) + { + List storesToRemove = new(); + HashSet modifiedContainers = new(); + + foreach (var invokeCall in parent.Descendants.OfType()) { 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 @@ public void Run(ILFunction function, ILTransformContext context) } 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 @@ public void Run(ILFunction function, ILTransformContext context) parentBlock.Instructions.RemoveAt(inst.ChildIndex); } - foreach (var container in modifiedContainers) - container.SortBlocks(deleteUnreachableBlocks: true); + return modifiedContainers; + } + + /// + /// 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. + /// + 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 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 deadArguments) + static ILInstruction MakeDynamicInstruction(CallSiteInfo callsite, CallVirt targetInvokeCall, List deadArguments) { switch (callsite.Kind) { @@ -272,7 +312,7 @@ ILInstruction MakeDynamicInstruction(CallSiteInfo callsite, CallVirt targetInvok } } - 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 @@ bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, I 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 @@ bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, I 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 @@ bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, I 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 @@ bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, I 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 @@ bool ScanCallSiteInitBlock(Block callSiteInitBlock, IField callSiteCacheField, I } } - 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 @@ bool ExtractArgumentInfo(ILInstruction value, ref CallSiteInfo callSiteInfo, int 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 @@ bool MatchCallSiteCacheNullCheck(ILInstruction condition, out IField callSiteCac return true; } - struct CallSiteInfo + internal struct CallSiteInfo { public bool Inverted; public ILInstruction BranchAfterInit; @@ -596,7 +636,7 @@ struct CallSiteInfo public string MemberName; } - enum BinderMethodKind + internal enum BinderMethodKind { BinaryOperation, Convert, diff --git a/ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs b/ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs index 13fdba2a08..4ff2c2df4b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs @@ -46,18 +46,25 @@ public void Run(ILFunction function, ILTransformContext context) 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; } } }