From e530b1c1b62713e0920eab4760c24222844af5dc Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 16 Aug 2026 10:07:26 +0200 Subject: [PATCH] Avoid `allowImplicitConversion: true` for `await` expressions -- there's no target type that the C# compiler could convert to. Instead, use `IsAppropriateCallTarget` to detect whether an explicit cast is necessary for calling the correct `GetAwaiter` method. --- ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 16 ++++++++++++++++ .../CSharp/ExpressionBuilder.cs | 7 +++++-- .../IL/ControlFlow/AsyncAwaitDecompiler.cs | 4 ++++ .../RuntimeAsyncManualAwaitTransform.cs | 1 + ICSharpCode.Decompiler/IL/Instructions/Await.cs | 3 +++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index b56fa2e48..60dfe1509 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1912,6 +1912,22 @@ namespace ICSharpCode.Decompiler.CSharp return false; } + + /// + /// Checks whether calling `target.methodName()` will use `expected` as the method to invoke. + /// + public bool CheckSimpleCall(ResolveResult target, IMethod expected, OpCode expectedCallOpCode = OpCode.Call) + { + var details = new ExpectedTargetDetails { CallOpCode = expectedCallOpCode, NeedsBoxingConversion = false }; + if (resolver.ResolveMemberAccess(target, expected.Name, [], NameLookupMode.InvocationTarget) + is not MethodGroupResolveResult mgrr) + return false; + var or = mgrr.PerformOverloadResolution(typeSystem, []); + if (or.BestCandidateErrors != OverloadResolutionErrors.None || or.IsAmbiguous) + return false; + return IsAppropriateCallTarget(details, expected, or.GetBestCandidateWithSubstitutedTypeArguments()!); + } + ExpressionWithResolveResult HandleConstructorCall(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, ArgumentList argumentList) { if (settings.AnonymousTypes && method.DeclaringType.IsAnonymousType()) diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 3f514ba73..75347a5e3 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4437,9 +4437,12 @@ namespace ICSharpCode.Decompiler.CSharp // we can deference the managed reference by stripping away the 'ref' value = value.UnwrapChild(((DirectionExpression)value.Expression).Expression); } - if (expectedType != null) + var callBuilder = new CallBuilder(this, typeSystem, settings); + if (expectedType != null + && inst.GetAwaiterMethod != null + && !callBuilder.CheckSimpleCall(value.ResolveResult, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) { - value = value.ConvertTo(expectedType, this, allowImplicitConversion: true); + value = value.ConvertTo(expectedType, this); } return new UnaryOperatorExpression(UnaryOperatorType.Await, value.Expression) .WithILInstruction(inst) diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs index 7b4e5df76..45b50af8f 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -1820,6 +1820,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow ILVariable awaiterVar = stLocAwaiter.Variable; ILInstruction awaitedValue; IMethod getAwaiterMethod; + OpCode getAwaiterCallOpCode; bool isDynamicAwait = false; if (stLocAwaiter.Value is CallInstruction getAwaiterCall && getAwaiterCall.Method.Name == "GetAwaiter" @@ -1828,6 +1829,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { awaitedValue = getAwaiterCall.Arguments[0]; getAwaiterMethod = getAwaiterCall.Method; + getAwaiterCallOpCode = getAwaiterCall.OpCode; } else if (stLocAwaiter.Value is DynamicInvokeMemberInstruction dynGetAwaiter && dynGetAwaiter.Name == "GetAwaiter" && dynGetAwaiter.Arguments.Count == 1) @@ -1836,6 +1838,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow awaitedValue = dynGetAwaiter.Arguments[0]; getAwaiterMethod = CreateDynamicAwaiterMethod(context, "GetAwaiter"); isDynamicAwait = true; + getAwaiterCallOpCode = OpCode.CallVirt; } else { @@ -1917,6 +1920,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Await awaitInst = new Await(UnwrapConvUnknown(awaitedValue)); awaitInst.GetResultMethod = getResultMethod; awaitInst.GetAwaiterMethod = getAwaiterMethod; + awaitInst.GetAwaiterCallOpCode = getAwaiterCallOpCode; getResultInst.ReplaceWith(awaitInst); // Remove useless reset of awaiterVar. diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs index 61bce9887..af724a533 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs @@ -162,6 +162,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow foreach (var inst in pauseBlock.Instructions) awaitInst.AddILRange(inst); awaitInst.GetAwaiterMethod = getAwaiterCall.Method; + awaitInst.GetAwaiterCallOpCode = getAwaiterCall.OpCode; awaitInst.GetResultMethod = getResultCall.Method; // Remove the trailing 3 (or 4) instructions of the head block; replace with `br completedBlock`. diff --git a/ICSharpCode.Decompiler/IL/Instructions/Await.cs b/ICSharpCode.Decompiler/IL/Instructions/Await.cs index d30032e09..b595807b0 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Await.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Await.cs @@ -25,5 +25,8 @@ namespace ICSharpCode.Decompiler.IL { public IMethod? GetAwaiterMethod; public IMethod? GetResultMethod; + // Whether the original GetAwaiter call was `call` or `callvirt`, so ExpressionBuilder can tell + // whether re-emitting it as a plain `await` expression would change which method gets invoked. + public OpCode GetAwaiterCallOpCode = OpCode.Call; } }