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; } } }