diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index 36f1d3981b..a00fc4425a 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -398,6 +398,12 @@ public async Task Async([ValueSource(nameof(noMonoOptions))] CompilerOptions opt await RunCS(options: options); } + [Test] + public async Task AsyncAwaitPatterns([ValueSource(nameof(noMonoOptions))] CompilerOptions options) + { + await RunCS(options: options); + } + [Test] public async Task LINQRaytracer([ValueSource(nameof(defaultOptions))] CompilerOptions options) { diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 3a16f4270c..bafa49605c 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -560,6 +560,12 @@ public async Task AsyncStreams([ValueSource(nameof(roslyn3OrNewerOptions))] Comp await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task AsyncAwaitPatterns([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task AsyncUsing([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/AsyncAwaitPatterns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/AsyncAwaitPatterns.cs new file mode 100644 index 0000000000..f545a820d0 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/AsyncAwaitPatterns.cs @@ -0,0 +1,228 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#pragma warning disable 1998 +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness +{ + // The Pretty fixture of the same name pins how awaits are printed; this one pins what they + // have to mean: copy semantics of struct awaitables, and the evaluation order around the + // suspension point. + public class AsyncAwaitPatterns + { + public struct CountingAwaitable + { + public int Counter; + + public TaskAwaiter GetAwaiter() + { + Counter++; + Console.WriteLine(" GetAwaiter, Counter is now " + Counter); + return Task.FromResult(0).GetAwaiter(); + } + } + + public class Holder + { + public CountingAwaitable Mutable; + public readonly CountingAwaitable ReadOnly; + + public CountingAwaitable Property { + get { return Mutable; } + } + } + + private int[] array = new int[4]; + private int index; + private int field; + + public static void Main() + { + new AsyncAwaitPatterns().Run().Wait(); + } + + public async Task Run() + { + await MutableStructField(); + await ReadOnlyStructField(); + await StructProperty(); + await CompoundAssignmentToArrayElement(); + await AssignmentAfterAwait(); + await ArgumentEvaluationOrder(); + await RefArgumentEvaluationOrder(); + await AwaitInLoop(); + await AwaitInTernary(true); + await AwaitInTernary(false); +#if CS60 + await AwaitInCatchAndFinally(); +#endif + Console.WriteLine("done"); + } + + private Task Value(int v) + { + Console.WriteLine(" Value(" + v + ")"); + return Task.FromResult(v); + } + + private int Index(string tag) + { + Console.WriteLine(" Index(" + tag + ") -> " + index); + return index; + } + + private int[] Array(string tag) + { + Console.WriteLine(" Array(" + tag + ")"); + return array; + } + + private int Side() + { + Console.WriteLine(" Side()"); + return 100; + } + + private static string Combine(int a, int b, int c) + { + return a + "/" + b + "/" + c; + } + + private static void AddTo(ref int slot, int addend) + { + Console.WriteLine(" AddTo(" + slot + ", " + addend + ")"); + slot += addend; + } + + // GetAwaiter is called on the field itself, so its mutation sticks. + public async Task MutableStructField() + { + Console.WriteLine("MutableStructField"); + Holder holder = new Holder(); + await holder.Mutable; + await holder.Mutable; + Console.WriteLine(" Counter = " + holder.Mutable.Counter); + } + + // A readonly field is defensively copied, so the mutation is discarded. + public async Task ReadOnlyStructField() + { + Console.WriteLine("ReadOnlyStructField"); + Holder holder = new Holder(); + await holder.ReadOnly; + await holder.ReadOnly; + Console.WriteLine(" Counter = " + holder.ReadOnly.Counter); + } + + // A property returns a copy, so the mutation is discarded as well. + public async Task StructProperty() + { + Console.WriteLine("StructProperty"); + Holder holder = new Holder(); + await holder.Property; + await holder.Property; + Console.WriteLine(" Counter = " + holder.Mutable.Counter); + } + + // Target and index are evaluated before the await, not after it. + public async Task CompoundAssignmentToArrayElement() + { + Console.WriteLine("CompoundAssignmentToArrayElement"); + array = new int[4]; + index = 0; + Array("lhs")[Index("lhs")] += await Value(5); + index = 1; + Console.WriteLine(" array = " + string.Join(",", array)); + } + + public async Task AssignmentAfterAwait() + { + Console.WriteLine("AssignmentAfterAwait"); + array = new int[4]; + index = 2; + int[] target = Array("target"); + int i = Index("i"); + index = 3; + target[i] = await Value(7); + Console.WriteLine(" array = " + string.Join(",", array)); + } + + public async Task ArgumentEvaluationOrder() + { + Console.WriteLine("ArgumentEvaluationOrder"); + Console.WriteLine(" " + Combine(await Value(1), Side(), await Value(2))); + } + + public async Task RefArgumentEvaluationOrder() + { + Console.WriteLine("RefArgumentEvaluationOrder"); + field = 0; + AddTo(ref field, await Value(6)); + Console.WriteLine(" field = " + field); + } + + public async Task AwaitInLoop() + { + Console.WriteLine("AwaitInLoop"); + for (int i = 0; i < 4; i++) + { + if (i == 1) + { + continue; + } + if (i == 3) + { + break; + } + Console.WriteLine(" loop " + await Value(i)); + } + } + + public async Task AwaitInTernary(bool condition) + { + Console.WriteLine("AwaitInTernary(" + condition + ")"); + Console.WriteLine(" " + (condition ? await Value(1) : await Value(2))); + } + +#if CS60 + public async Task AwaitInCatchAndFinally() + { + Console.WriteLine("AwaitInCatchAndFinally"); + try + { + await Value(1); + throw new InvalidOperationException("boom"); + } + catch (InvalidOperationException ex) + { + Console.WriteLine(" caught " + ex.Message); + await Value(2); + } + finally + { + Console.WriteLine(" finally"); + await Value(3); + } + } +#endif + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs new file mode 100644 index 0000000000..ede40cd290 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs @@ -0,0 +1,697 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Await shapes that still decompile to code that does not compile are tracked as #4017 (type +// parameter with an interface constraint), #4018 (static dynamic call) and #4019 (with +// expression); they are absent here because they have no correct output to pin yet. + +#pragma warning disable 1998 +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwait +{ + public class AwaitableContainer + { + public class NestedAwaitable + { + public TaskAwaiter GetAwaiter() + { + return default(TaskAwaiter); + } + } + } + + /// + /// The context the await sits in: how the translated expression has to be parenthesized, and + /// where the async state machine splits the surrounding statement. + /// + public class AwaitContexts + { +#if CS80 && !NET40 + private sealed class AsyncDisposable : IAsyncDisposable + { + public ValueTask DisposeAsync() + { + return default(ValueTask); + } + } +#endif + + private static Task Get() + { + return Task.FromResult(1); + } + + private static Task GetString() + { + return Task.FromResult("s"); + } + + private static Task GetException() + { + return Task.FromResult(new Exception()); + } + + public async Task Statement() + { + await Get(); + } + + public async Task Argument() + { + Console.WriteLine(await Get()); + } + + public async Task BinaryOperator() + { +#if ROSLYN2 || OPT + Console.WriteLine(await Get() + await Get()); +#else + int value = await Get() + await Get(); + Console.WriteLine(value); +#endif + } + + public async Task UnaryOperator() + { +#if ROSLYN2 || OPT + Console.WriteLine(-(await Get())); +#else + int value = -(await Get()); + Console.WriteLine(value); +#endif + } + + public async Task MemberAccessOnResult() + { +#if ROSLYN2 || OPT + Console.WriteLine((await GetString()).Length); +#else + int length = (await GetString()).Length; + Console.WriteLine(length); +#endif + } + + public async Task IndexerOnResult() + { +#if ROSLYN2 || OPT + Console.WriteLine((await GetString())[0]); +#else + char value = (await GetString())[0]; + Console.WriteLine(value); +#endif + } + + public async Task CoalesceOnResult() + { +#if ROSLYN2 || OPT + Console.WriteLine((await GetString()) ?? "null"); +#else + string value = (await GetString()) ?? "null"; + Console.WriteLine(value); +#endif + } + + public async Task ThrowAwaitedException() + { + throw await GetException(); + } + + public async Task Checked() + { +#if ROSLYN2 || OPT + Console.WriteLine(checked(await Get() + 1)); +#else + int value = checked(await Get() + 1); + Console.WriteLine(value); +#endif + } + +#if CS60 + public async Task TryFinally() + { + try + { + await Get(); + } + finally + { + await Get(); + } + } +#endif + + public async Task Using() + { + using (new Disposable()) + { + await Get(); + } + } + +#if CS60 + public async Task ConditionalAccessOnResult() + { +#if ROSLYN2 || OPT + Console.WriteLine((await GetString())?.Length); +#else + object value = (await GetString())?.Length; + Console.WriteLine(value); +#endif + } + + public async Task CatchWithFilter() + { + try + { + await Get(); + } + catch (Exception ex) when (ex.Message.Length > 2) + { + await Get(); + } + } +#endif + +#if CS70 && !NET40 + public async Task AwaitInTupleLiteral() + { + Console.WriteLine((await Get(), await GetString())); + } +#endif + +#if CS80 && !NET40 + public async Task AwaitUsing() + { + await using (new AsyncDisposable()) + { + await Get(); + } + } + + public async Task AwaitForeach(IAsyncEnumerable source) + { + await foreach (int item in source) + { + Console.WriteLine(item); + } + } + + public async Task AwaitForeachConfigured(IAsyncEnumerable source) + { + await foreach (int item in source.ConfigureAwait(continueOnCapturedContext: false)) + { + Console.WriteLine(item); + } + } + + public async IAsyncEnumerable AsyncIterator() + { + yield return await Get(); + await Task.Yield(); + yield return 2; + } + + public async IAsyncEnumerable AsyncIteratorWithFinally() + { + try + { + yield return await Get(); + } + finally + { + Console.WriteLine("cleanup"); + } + } + + public async Task LocalFunction() + { + Console.WriteLine(await Local()); + static async Task Local() + { + return await Get(); + } + } +#endif + + public async Task AwaitInGenericMethod(Task task) + { +#if ROSLYN2 || OPT + Console.WriteLine(await task); +#else + object value = await task; + Console.WriteLine(value); +#endif + } + } + + public static class AwaiterExtensions + { + public static TaskAwaiter GetAwaiter(this IAwaitableMarker marker) + { + return default(TaskAwaiter); + } + + public static TaskAwaiter GetAwaiter(this int millisecondsDelay) + { + return Task.Delay(millisecondsDelay).GetAwaiter(); + } + + public static TaskAwaiter GetAwaiter(this Action action) + { + return default(TaskAwaiter); + } + + public static TaskAwaiter GetAwaiter(this IEnumerable> tasks) + { + return default(TaskAwaiter); + } + +#if CS70 && !NET40 + public static TaskAwaiter GetAwaiter(this (Task, string) taggedTask) + { + return taggedTask.Item1.GetAwaiter(); + } +#endif + +#if CS72 + public static TaskAwaiter GetAwaiter(this in ByRefReceiver receiver) + { + return receiver.Self(); + } +#endif + } + + /// + /// The operand side: the shape of the expression the await is applied to. + /// + public class AwaitOperands + { + private Task taskField; + + private StructAwaitable structField; + + private readonly StructAwaitable readonlyStructField; + + private Task Property { + get { + Console.WriteLine("get_Property"); + return taskField; + } + } + + private Task this[int index] { + get { + Console.WriteLine("get_Item"); + return taskField; + } + } + + private static Task Get() + { + return Task.FromResult(1); + } + + public async Task DefaultOfStruct() + { + await default(StructAwaitable); + } + + public async Task Ternary(bool condition, Task first, Task second) + { + await (condition ? first : second); + } + + public async Task Coalesce(Task first, Task second) + { + await (first ?? second); + } + +#if CS60 + public async Task NullConditional(List tasks) + { + await (tasks?[0]); + } +#endif + + public async Task Cast(object obj) + { + await (Task)obj; + } + + /// + /// A null literal has no type, so the cast is what makes the operand awaitable. Note that + /// default(Task) compiles to the same `ldnull` and therefore decompiles to this same + /// cast; the two are indistinguishable in IL. + /// + public async Task NullLiteral() + { + await (Task)null; + } + + public async Task AsOperator(object obj) + { + await (obj as Task); + } + + public async Task FieldAccess() + { + Console.WriteLine(await taskField); + } + + public async Task PropertyAccess() + { + Console.WriteLine(await Property); + } + + public async Task IndexerAccess() + { + Console.WriteLine(await this[0]); + } + + public async Task StructField() + { + await structField; + } + + public async Task ReadOnlyStructField() + { + await readonlyStructField; + } + + public async Task StructArrayElement(StructAwaitable[] awaitables) + { + await awaitables[0]; + } + + public async Task MethodCall() + { + Console.WriteLine(await Get()); + } + + public async Task DelegateInvocation(Func> factory) + { + Console.WriteLine(await factory()); + } + + public async Task ArrayElement(Task[] tasks) + { + Console.WriteLine(await tasks[0]); + } + + public async Task TernaryOfTasks(bool condition) + { + Console.WriteLine(await (condition ? Get() : Get())); + } + } + + /// + /// The receiver ("expected type") side of ExpressionBuilder.VisitAwait: the awaited expression + /// is converted to the declaring type of the resolved GetAwaiter, or to its first parameter + /// type when GetAwaiter is an extension method. + /// + public class AwaitReceivers + { + public async Task InstanceAwaiterOnSelf(ClassAwaitable awaitable) + { + await awaitable; + } + + public async Task AwaiterInheritedFromBaseClass(DerivedAwaitable awaitable) + { + await awaitable; + } + + public async Task AwaiterThroughInterface(IAwaitable awaitable) + { + await awaitable; + } + + public async Task AwaiterThroughBaseInterface(IDerivedAwaitable awaitable) + { + await awaitable; + } + + /// + /// The cast carries the operand to the interface that declares GetAwaiter; without it the + /// explicit implementation is not accessible. A conversion that is merely implicit must not + /// be dropped here, even though a boxing conversion exists. + /// + public async Task ExplicitInterfaceImplementationOnStruct(ExplicitStructAwaitable awaitable) + { + await (IAwaitable)awaitable; + } + + /// + /// The same shape on a class, i.e. it is not specific to the boxing conversion. + /// + public async Task ExplicitInterfaceImplementationOnClass(ExplicitClassAwaitable awaitable) + { + await (IAwaitable)awaitable; + } + + /// + /// The await pattern does not apply user-defined conversions, so the cast that invokes + /// op_Implicit has to survive. + /// + public async Task UserDefinedConversionToAwaitable(ConvertsToAwaitable awaitable) + { + await (ClassAwaitable)awaitable; + } + + public async Task ExtensionAwaiterOnClass(MarkerClass marker) + { + await marker; + } + + public async Task ExtensionAwaiterOnStruct(MarkerStruct marker) + { + await marker; + } + + public async Task ExtensionAwaiterOnPrimitive() + { + await 100; + } + + public async Task ExtensionAwaiterOnDelegate(Action action) + { + await action; + } + +#if CS72 + /// + /// An extension GetAwaiter taking its receiver by 'in' makes the expected type a + /// ByReferenceType. Stripping the 'ref' must not leave a conversion that reaches the + /// managed reference back through a pointer. + /// + public async Task InReceiverExtensionAwaiter(ByRefReceiver receiver) + { + await receiver; + } +#endif + + public async Task ExtensionAwaiterOverTaskArray(Task[] tasks) + { +#if ROSLYN2 || OPT + Console.WriteLine((await tasks)[0]); +#else + int value = (await tasks)[0]; + Console.WriteLine(value); +#endif + } + + public async Task ExtensionAwaiterOverTaskList(List> tasks) + { +#if ROSLYN2 || OPT + Console.WriteLine((await tasks)[0]); +#else + int value = (await tasks)[0]; + Console.WriteLine(value); +#endif + } + + public async Task GenericAwaitableType(GenericAwaitable awaitable) + { + Console.WriteLine(await awaitable); + } + + public async Task NestedAwaitableType(AwaitableContainer.NestedAwaitable awaitable) + { + await awaitable; + } + + public async Task TypeParameterWithClassConstraint(T awaitable) where T : ClassAwaitable + { + await awaitable; + } + + public async Task TypeParameterWithStructConstraint(T awaitable) where T : struct, IAwaitable + { + await awaitable; + } + + public async Task ConfiguredTaskAwaitable(Task task) + { +#if ROSLYN2 + Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false)); +#else + Console.WriteLine(await task.ConfigureAwait(false)); +#endif + } + +#if CS70 && !NET40 + public async Task ExtensionAwaiterOnTuple(Task task) + { + Console.WriteLine(await (task, "tag")); + } +#endif + +#if CS80 && !NET40 + public async Task ValueTaskAwaitable(ValueTask task) + { + Console.WriteLine(await task); + } + + public async Task ConfiguredValueTaskAwaitable(ValueTask task) + { + Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false)); + } +#endif + +#if NET80 + public async Task ConfigureAwaitWithOptions(Task task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } +#endif + + public async Task AwaitOfAwait(Task> task) + { + Console.WriteLine(await (await task)); + } + } + +#if CS72 + public struct ByRefReceiver + { + public long A; + + public long B; + + public TaskAwaiter Self() + { + return default(TaskAwaiter); + } + } +#endif + + public class ClassAwaitable : IAwaitable + { + public TaskAwaiter GetAwaiter() + { + return default(TaskAwaiter); + } + } + + public class ConvertsToAwaitable + { + public static implicit operator ClassAwaitable(ConvertsToAwaitable value) + { + return new ClassAwaitable(); + } + } + + public class DerivedAwaitable : ClassAwaitable + { + } + + public class Disposable : IDisposable + { + public void Dispose() + { + } + } + + public class ExplicitClassAwaitable : IAwaitable + { + TaskAwaiter IAwaitable.GetAwaiter() + { + return default(TaskAwaiter); + } + } + + [StructLayout(LayoutKind.Sequential, Size = 1)] + public struct ExplicitStructAwaitable : IAwaitable + { + TaskAwaiter IAwaitable.GetAwaiter() + { + return default(TaskAwaiter); + } + } + + public class GenericAwaitable + { + public TaskAwaiter GetAwaiter() + { + return default(TaskAwaiter); + } + } + + public interface IAwaitable + { + TaskAwaiter GetAwaiter(); + } + + public interface IAwaitableMarker + { + } + + public interface IBaseAwaitable + { + TaskAwaiter GetAwaiter(); + } + + public interface IDerivedAwaitable : IBaseAwaitable + { + } + + public class MarkerClass : IAwaitableMarker + { + } + + [StructLayout(LayoutKind.Sequential, Size = 1)] + public struct MarkerStruct : IAwaitableMarker + { + } + + public struct StructAwaitable : IAwaitable + { + public int Counter; + + public TaskAwaiter GetAwaiter() + { + Counter++; + return default(TaskAwaiter); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index b56fa2e489..60dfe15098 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1912,6 +1912,22 @@ bool IsAppropriateCallTarget(ExpectedTargetDetails expectedTargetDetails, IMembe 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 3f514ba731..a59e50abf8 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4437,9 +4437,30 @@ protected internal override TranslatedExpression VisitAwait(Await inst, Translat // 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) { - value = value.ConvertTo(expectedType, this, allowImplicitConversion: true); + // An operand boxed for the GetAwaiter call is typed 'object', which hides the receiver + // from member lookup. C# boxes the operand of an `await` implicitly, so the box need + // not appear in the output as long as the unboxed operand still binds the same + // GetAwaiter. Look through the box for that question only; UnwrapChild detaches the + // operand from the AST, so it must not run before the answer is known. + Expression? boxedOperand = null; + var lookupTarget = value.ResolveResult; + if (value.ResolveResult is ConversionResolveResult { Conversion.IsBoxingConversion: true } boxing + && value.Expression is CastExpression boxCast) + { + boxedOperand = boxCast.Expression; + lookupTarget = boxing.Input; + } + if (!callBuilder.CheckSimpleCall(lookupTarget, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) + { + value = value.ConvertTo(expectedType, this); + } + else if (boxedOperand != null) + { + value = value.UnwrapChild(boxedOperand); + } } 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 7b4e5df76c..45b50af8f9 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -1820,6 +1820,7 @@ void DetectAwaitPattern(Block block) 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 @@ void DetectAwaitPattern(Block block) { 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 @@ void DetectAwaitPattern(Block block) awaitedValue = dynGetAwaiter.Arguments[0]; getAwaiterMethod = CreateDynamicAwaiterMethod(context, "GetAwaiter"); isDynamicAwait = true; + getAwaiterCallOpCode = OpCode.CallVirt; } else { @@ -1917,6 +1920,7 @@ void DetectAwaitPattern(Block block) 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 61bce98877..af724a5332 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs @@ -162,6 +162,7 @@ static bool DetectRuntimeAsyncManualAwait(Block block, ILTransformContext contex 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 d30032e09f..b595807b04 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Await.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Await.cs @@ -25,5 +25,8 @@ partial class Await { 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; } }