Browse Source

Add an async/await pattern test suite for ExpressionBuilder.VisitAwait

The await surface had almost no fixture coverage beyond Task/ValueTask: every
GetAwaiter in the corpus was an instance method on the awaited type itself, so
the conversion VisitAwait applies to the operand was never exercised for an
inherited, interface-typed or extension-method awaiter. Probing that surface
turned up eight defects, all of which produce C# that does not compile.

AsyncAwaitPatterns pins the shapes that do round-trip, along the three axes the
translation actually depends on: the GetAwaiter receiver, the operand
expression, and the context the await sits in. Its Correctness twin pins what
Pretty cannot see - copy semantics of struct awaitables and the evaluation
order around the suspension point.

AsyncAwaitPatternsBugs is the spec for the defects, written as the C# that
ought to come out, with the current wrong output named per member. It fails
today; that is the point, and fixing a defect is meant to delete a comment
rather than edit an expectation.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4021/head
Siegfried Pammer 1 month ago committed by Daniel Grunwald
parent
commit
d8aca3f113
  1. 6
      ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs
  2. 16
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 228
      ICSharpCode.Decompiler.Tests/TestCases/Correctness/AsyncAwaitPatterns.cs
  4. 598
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs
  5. 185
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs

6
ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs

@ -398,6 +398,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -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)
{

16
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -560,6 +560,22 @@ namespace ICSharpCode.Decompiler.Tests @@ -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)
{

228
ICSharpCode.Decompiler.Tests/TestCases/Correctness/AsyncAwaitPatterns.cs

@ -0,0 +1,228 @@ @@ -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<int> 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<int> 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
}
}

598
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs

@ -0,0 +1,598 @@ @@ -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);
}
}
}
/// <summary>
/// The context the await sits in: how the translated expression has to be parenthesized, and
/// where the async state machine splits the surrounding statement.
/// </summary>
public class AwaitContexts
{
#if CS80 && !NET40
private sealed class AsyncDisposable : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
return default(ValueTask);
}
}
#endif
private static Task<int> Get()
{
return Task.FromResult(1);
}
private static Task<string> GetString()
{
return Task.FromResult("s");
}
private static Task<Exception> 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<int> source)
{
await foreach (int item in source)
{
Console.WriteLine(item);
}
}
public async Task AwaitForeachConfigured(IAsyncEnumerable<int> source)
{
await foreach (int item in source.ConfigureAwait(continueOnCapturedContext: false))
{
Console.WriteLine(item);
}
}
public async IAsyncEnumerable<int> AsyncIterator()
{
yield return await Get();
await Task.Yield();
yield return 2;
}
public async IAsyncEnumerable<int> AsyncIteratorWithFinally()
{
try
{
yield return await Get();
}
finally
{
Console.WriteLine("cleanup");
}
}
public async Task LocalFunction()
{
Console.WriteLine(await Local());
static async Task<int> Local()
{
return await Get();
}
}
#endif
public async Task AwaitInGenericMethod<T>(Task<T> 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<T[]> GetAwaiter<T>(this IEnumerable<Task<T>> tasks)
{
return default(TaskAwaiter<T[]>);
}
#if CS70 && !NET40
public static TaskAwaiter<T> GetAwaiter<T>(this (Task<T>, string) taggedTask)
{
return taggedTask.Item1.GetAwaiter();
}
#endif
}
/// <summary>
/// The operand side: the shape of the expression the await is applied to.
/// </summary>
public class AwaitOperands
{
private Task<int> taskField;
private StructAwaitable structField;
private readonly StructAwaitable readonlyStructField;
private Task<int> Property {
get {
Console.WriteLine("get_Property");
return taskField;
}
}
private Task<int> this[int index] {
get {
Console.WriteLine("get_Item");
return taskField;
}
}
private static Task<int> 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<Task> 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<Task<int>> factory)
{
Console.WriteLine(await factory());
}
public async Task ArrayElement(Task<int>[] tasks)
{
Console.WriteLine(await tasks[0]);
}
public async Task TernaryOfTasks(bool condition)
{
Console.WriteLine(await (condition ? Get() : Get()));
}
}
/// <summary>
/// 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.
/// </summary>
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<int>[] tasks)
{
#if ROSLYN2 || OPT
Console.WriteLine((await tasks)[0]);
#else
int value = (await tasks)[0];
Console.WriteLine(value);
#endif
}
public async Task ExtensionAwaiterOverTaskList(List<Task<int>> tasks)
{
#if ROSLYN2 || OPT
Console.WriteLine((await tasks)[0]);
#else
int value = (await tasks)[0];
Console.WriteLine(value);
#endif
}
public async Task GenericAwaitableType(GenericAwaitable<string> awaitable)
{
Console.WriteLine(await awaitable);
}
public async Task NestedAwaitableType(AwaitableContainer.NestedAwaitable awaitable)
{
await awaitable;
}
public async Task TypeParameterWithClassConstraint<T>(T awaitable) where T : ClassAwaitable
{
await awaitable;
}
public async Task TypeParameterWithStructConstraint<T>(T awaitable) where T : struct, IAwaitable
{
await awaitable;
}
public async Task ConfiguredTaskAwaitable(Task<int> 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<int> task)
{
Console.WriteLine(await (task, "tag"));
}
#endif
#if CS80 && !NET40
public async Task ValueTaskAwaitable(ValueTask<int> task)
{
Console.WriteLine(await task);
}
public async Task ConfiguredValueTaskAwaitable(ValueTask<int> 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<int>> 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<T>
{
public TaskAwaiter<T> GetAwaiter()
{
return default(TaskAwaiter<T>);
}
}
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);
}
}
}

185
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs

@ -0,0 +1,185 @@ @@ -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<int> Get()
{
return Task.FromResult(1);
}
/// <summary>
/// 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: <c>await value;</c> -> CS1929.
/// </summary>
public async Task ExplicitInterfaceImplementationOnStruct(ExplicitStructAwaitable value)
{
await (IAwaitable)value;
}
/// <summary>
/// Same defect on a class, i.e. it is not specific to the boxing conversion.
/// Today: <c>await value;</c> -> CS1929.
/// </summary>
public async Task ExplicitInterfaceImplementationOnClass(ExplicitClassAwaitable value)
{
await (IAwaitable)value;
}
/// <summary>
/// The await pattern does not apply user-defined conversions, so the cast that invokes
/// op_Implicit has to survive.
/// Today: <c>await value;</c> -> CS1929.
/// </summary>
public async Task UserDefinedConversionToAwaitable(ConvertsToAwaitable value)
{
await (ClassAwaitable)value;
}
/// <summary>
/// A null literal has no type, so the cast is what makes the operand awaitable.
/// Today: <c>await null;</c> -> CS4001 "Cannot await '&lt;null&gt;'".
/// </summary>
public async Task AwaitNullTask()
{
await (Task)null;
}
/// <summary>
/// Today: <c>await null;</c> -> CS4001, i.e. <c>default(Task)</c> is lost the same way.
/// </summary>
public async Task AwaitDefaultTask()
{
await default(Task);
}
/// <summary>
/// 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: <c>public unsafe async Task ...</c> with <c>await (ref *(ByRefReceiver*)value);</c>
/// -> CS1525.
/// </summary>
public async Task InReceiverExtensionAwaiter(ByRefReceiver value)
{
await value;
}
/// <summary>
/// The constrained callvirt lowers to an LdObjIfRef that ExpressionBuilder has no case
/// for, and the operand is dropped entirely.
/// Today: <c>await (IAwaitable)/*OpCode not supported: LdObjIfRef*/;</c> -> CS0119.
/// </summary>
public async Task TypeParameterWithInterfaceConstraint<T>(T value) where T : IAwaitable
{
await value;
}
/// <summary>
/// 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: <c>Type typeFromHandle = typeof(Console); typeFromHandle.WriteLine(...);</c>
/// -> CS1061. Without the await (or for an instance call) the same code is correct.
/// </summary>
public async Task DynamicAwaitInStaticCall(dynamic value)
{
Console.WriteLine("x" + await value);
}
/// <summary>
/// The await splits the assignment across a suspension point, which defeats the
/// with-expression transform and leaves the raw clone call behind.
/// Today: <c>Record record = value._003CClone_003E_0024();</c> -> uncompilable.
/// Without the await the same expression round-trips.
/// </summary>
public async Task<Record> 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);
}
Loading…
Cancel
Save