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)