diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index 36f1d3981..a00fc4425 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -398,6 +398,12 @@ namespace ICSharpCode.Decompiler.Tests 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 3a16f4270..192c71f8f 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -560,6 +560,22 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task AsyncAwaitPatterns([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + + [Test] + public async Task AsyncAwaitPatternsBugs([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + // The fixture is the spec: it is written as the C# the decompiler ought to produce. + // Every one of its members currently decompiles to something that does not compile; + // the file names the wrong output per member. This test is expected to fail until + // those defects are fixed. + 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 000000000..f545a820d --- /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 000000000..4a329293d --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs @@ -0,0 +1,598 @@ +// 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.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 + } + + /// + /// 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; + } + + 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; + } + + 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; + } + + 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)); + } + } + + public class ClassAwaitable : IAwaitable + { + public TaskAwaiter GetAwaiter() + { + return default(TaskAwaiter); + } + } + + public class DerivedAwaitable : ClassAwaitable + { + } + + public class Disposable : IDisposable + { + public void Dispose() + { + } + } + + 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.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs new file mode 100644 index 000000000..3f93cefbd --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs @@ -0,0 +1,185 @@ +// 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. + +// Every member of this file is an await shape whose decompilation does not compile today. +// The file is written as the SPEC: input == expected output == correct C#, so a fixed +// decompiler makes the test pass with no edits here. Each member names the output that is +// produced instead. The test is ignored until all of them are fixed. + +#pragma warning disable 1998 +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs +{ + public class AwaitPatternsThatDoNotRoundTrip + { + private static Task Get() + { + return Task.FromResult(1); + } + + /// + /// The cast carries the operand to the interface that declares GetAwaiter; without it the + /// explicit implementation is not accessible. ConvertTo(allowImplicitConversion: true) + /// drops it because a boxing conversion exists. + /// Today: await value; -> CS1929. + /// + public async Task ExplicitInterfaceImplementationOnStruct(ExplicitStructAwaitable value) + { + await (IAwaitable)value; + } + + /// + /// Same defect on a class, i.e. it is not specific to the boxing conversion. + /// Today: await value; -> CS1929. + /// + public async Task ExplicitInterfaceImplementationOnClass(ExplicitClassAwaitable value) + { + await (IAwaitable)value; + } + + /// + /// The await pattern does not apply user-defined conversions, so the cast that invokes + /// op_Implicit has to survive. + /// Today: await value; -> CS1929. + /// + public async Task UserDefinedConversionToAwaitable(ConvertsToAwaitable value) + { + await (ClassAwaitable)value; + } + + /// + /// A null literal has no type, so the cast is what makes the operand awaitable. + /// Today: await null; -> CS4001 "Cannot await '<null>'". + /// + public async Task AwaitNullTask() + { + await (Task)null; + } + + /// + /// Today: await null; -> CS4001, i.e. default(Task) is lost the same way. + /// + public async Task AwaitDefaultTask() + { + await default(Task); + } + + /// + /// An extension GetAwaiter taking its receiver by 'in' makes the expected type a + /// ByReferenceType; VisitAwait strips the DirectionExpression and ConvertTo then converts + /// the value back to a managed reference through a pointer. + /// Today: public unsafe async Task ... with await (ref *(ByRefReceiver*)value); + /// -> CS1525. + /// + public async Task InReceiverExtensionAwaiter(ByRefReceiver value) + { + await value; + } + + /// + /// The constrained callvirt lowers to an LdObjIfRef that ExpressionBuilder has no case + /// for, and the operand is dropped entirely. + /// Today: await (IAwaitable)/*OpCode not supported: LdObjIfRef*/; -> CS0119. + /// + public async Task TypeParameterWithInterfaceConstraint(T value) where T : IAwaitable + { + await value; + } + + /// + /// A dynamic call to a static method whose argument list contains an await: the + /// typeof(TargetType) marker of the call site is materialized as the receiver. + /// Today: Type typeFromHandle = typeof(Console); typeFromHandle.WriteLine(...); + /// -> CS1061. Without the await (or for an instance call) the same code is correct. + /// + public async Task DynamicAwaitInStaticCall(dynamic value) + { + Console.WriteLine("x" + await value); + } + + /// + /// The await splits the assignment across a suspension point, which defeats the + /// with-expression transform and leaves the raw clone call behind. + /// Today: Record record = value._003CClone_003E_0024(); -> uncompilable. + /// Without the await the same expression round-trips. + /// + public async Task WithExpressionContainingAwait(Record value) + { + return value with { + X = await Get() + }; + } + } + public static class ByRefAwaiterExtensions + { + public static TaskAwaiter GetAwaiter(this in ByRefReceiver receiver) + { + return receiver.Self(); + } + } + public struct ByRefReceiver + { + public long A; + + public long B; + + public TaskAwaiter Self() + { + return default(TaskAwaiter); + } + } + 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 ExplicitClassAwaitable : IAwaitable + { + TaskAwaiter IAwaitable.GetAwaiter() + { + return default(TaskAwaiter); + } + } + [StructLayout(LayoutKind.Sequential, Size = 1)] + public struct ExplicitStructAwaitable : IAwaitable + { + TaskAwaiter IAwaitable.GetAwaiter() + { + return default(TaskAwaiter); + } + } + public interface IAwaitable + { + TaskAwaiter GetAwaiter(); + } + + public record Record(int X); +}