diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs new file mode 100644 index 0000000..858f8e6 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs @@ -0,0 +1,710 @@ +// 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() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } + } + + /// + /// 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; + } + } +#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()); + } + +#if ROSLYN2 || OPT + public async Task BinaryOperator() + { + Console.WriteLine(await Get() + await Get()); + } +#endif + +#if ROSLYN2 || OPT + public async Task UnaryOperator() + { + Console.WriteLine(-(await Get())); + } +#endif + +#if ROSLYN2 || OPT + public async Task MemberAccessOnResult() + { + Console.WriteLine((await GetString()).Length); + } +#endif + +#if ROSLYN2 || OPT + public async Task IndexerOnResult() + { + Console.WriteLine((await GetString())[0]); + } +#endif + +#if ROSLYN2 || OPT + public async Task CoalesceOnResult() + { + Console.WriteLine((await GetString()) ?? "null"); + } +#endif + + public async Task ThrowAwaitedException() + { + throw await GetException(); + } + +#if ROSLYN2 || OPT + public async Task Checked() + { + Console.WriteLine(checked(await Get() + 1)); + } +#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 + +#if ROSLYN2 || OPT + public async Task AwaitInGenericMethod(Task task) + { + Console.WriteLine(await task); + } +#endif + } + + public static class AwaiterExtensions + { + public static TaskAwaiter GetAwaiter(this IAwaitableMarker marker) + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + + public static TaskAwaiter GetAwaiter(this int millisecondsDelay) + { + return Task.Delay(millisecondsDelay).GetAwaiter(); + } + + public static TaskAwaiter GetAwaiter(this Action action) + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + + public static TaskAwaiter GetAwaiter(this IEnumerable> tasks) + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + +#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 + +#if ROSLYN2 + public async Task ExtensionAwaiterOverTaskArray(Task[] tasks) + { + Console.WriteLine((await tasks)[0]); + } +#endif + +#if ROSLYN2 + public async Task ExtensionAwaiterOverTaskList(List> tasks) + { + Console.WriteLine((await tasks)[0]); + } +#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() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } +#endif + + public class ClassAwaitable : IAwaitable + { + public TaskAwaiter GetAwaiter() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } + + 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() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } + + [StructLayout(LayoutKind.Sequential, Size = 1)] + public struct ExplicitStructAwaitable : IAwaitable + { + TaskAwaiter IAwaitable.GetAwaiter() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } + + public class GenericAwaitable + { + public TaskAwaiter GetAwaiter() + { +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } + + 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++; +#if CS71 + return default; +#else + return default(TaskAwaiter); +#endif + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index e2af27a..5655ce3 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -3793,7 +3793,7 @@ namespace ICSharpCode.Decompiler.CSharp { var property = (IProperty)lastElement.Member; Debug.Assert(property.Parameters.Count == indices); - Debug.Assert(property.Setter != null, $"Indexer property {property} has no setter"); + DecompilerAssert.That(property.Setter != null, $"Indexer property {property} has no setter"); elementsStack.Peek().Add( new CallBuilder(this, typeSystem, settings) .BuildDictionaryInitializerExpression(lastElement.OpCode, property.Setter, initObjRR, GetIndices(lastElement.Indices, indexVariables).ToList(), info.Values!.Single()) @@ -4060,13 +4060,22 @@ namespace ICSharpCode.Decompiler.CSharp TranslatedExpression TranslateWithInitializer(Block block) { - var stloc = block.Instructions.FirstOrDefault() as StLoc; - var final = block.FinalInstruction as LdLoc; - if (stloc == null || final == null || stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget) + if (block.Instructions.FirstOrDefault() is not StLoc stloc + || block.FinalInstruction is not LdLoc final + || stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget) + { throw new ArgumentException("given Block is invalid!"); + } + ILInstruction oldValue = stloc.Value; + if (stloc.Variable.Type.IsReferenceType == true) + { + // reference types must involve a $() call + Debug.Assert(TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall((CallInstruction)stloc.Value)); + oldValue = ((CallInstruction)stloc.Value).Arguments.Single(); + } WithInitializerExpression withInitializerExpression = new WithInitializerExpression(); - withInitializerExpression.Expression = Translate(stloc.Value, stloc.Variable.Type); + withInitializerExpression.Expression = Translate(oldValue, stloc.Variable.Type); withInitializerExpression.Initializer = BuildArrayInitializerExpression(block, new InitializedObjectResolveResult(stloc.Variable.Type)); return withInitializerExpression.WithILInstruction(block)