Browse Source

Track the await shapes that still fail as issues, not as a red test

The fixture was written as a spec of nine await shapes that decompiled to code
that does not compile. Six no longer do. Of the rest, default(Task) was never a
defect -- it compiles to the same ldnull as (Task)null, so the two are
indistinguishable in IL and the cast is a correct decompilation. The three real
ones are unrelated to the await conversion and have no correct output to pin
yet, so they move to #4017, #4018 and #4019; what stays behind is a regression
test for the shapes where the cast in front of the operand is load-bearing.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4021/head
Christoph Wille 1 month ago committed by Daniel Grunwald
parent
commit
d0c9cc9bae
  1. 4
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 82
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatternsBugs.cs

4
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -569,10 +569,6 @@ namespace ICSharpCode.Decompiler.Tests @@ -569,10 +569,6 @@ namespace ICSharpCode.Decompiler.Tests
[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);
}

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

@ -16,13 +16,16 @@ @@ -16,13 +16,16 @@
// 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.
// Await shapes where the cast in front of the operand is load-bearing: dropping it either makes
// GetAwaiter unreachable or leaves the operand with no type at all. Each member here once
// decompiled to code that does not compile, so the file doubles as a regression test - it is
// written as the C# the decompiler has to produce, and a relapse shows up as a diff.
//
// Await shapes that still decompile to uncompilable code are tracked as #4017 (type parameter
// with an interface constraint), #4018 (static dynamic call) and #4019 (with expression); they
// are not covered here because they have no correct output to pin yet.
#pragma warning disable 1998
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@ -31,16 +34,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs @@ -31,16 +34,10 @@ 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.
/// explicit implementation is not accessible. A conversion that is merely implicit must not
/// be dropped here, even though a boxing conversion exists.
/// </summary>
public async Task ExplicitInterfaceImplementationOnStruct(ExplicitStructAwaitable value)
{
@ -48,8 +45,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs @@ -48,8 +45,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs
}
/// <summary>
/// Same defect on a class, i.e. it is not specific to the boxing conversion.
/// Today: <c>await value;</c> -> CS1929.
/// The same shape on a class, i.e. it is not specific to the boxing conversion.
/// </summary>
public async Task ExplicitInterfaceImplementationOnClass(ExplicitClassAwaitable value)
{
@ -59,7 +55,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs @@ -59,7 +55,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs
/// <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)
{
@ -67,67 +62,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs @@ -67,67 +62,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs
}
/// <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;'".
/// A null literal has no type, so the cast is what makes the operand awaitable. Note that
/// <c>default(Task)</c> compiles to the same `ldnull` and therefore decompiles to this same
/// cast; the two are indistinguishable in IL.
/// </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.
/// ByReferenceType. Stripping the 'ref' must not leave a conversion that reaches the
/// managed reference back through a pointer.
/// </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
{
@ -180,6 +132,4 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs @@ -180,6 +132,4 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwaitBugs
{
TaskAwaiter GetAwaiter();
}
public record Record(int X);
}

Loading…
Cancel
Save