mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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 Codepull/4021/head
5 changed files with 1033 additions and 0 deletions
@ -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
|
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 '<null>'".
|
||||||
|
/// </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…
Reference in new issue