Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cf23d76
Add tests
siegfriedpammer Apr 12, 2026
82edef3
Add scaffolding for the C# 15 runtime-async lowering.
siegfriedpammer Apr 15, 2026
1a297be
Handle non-Task task-likes (ValueTask, custom AsyncMethodBuilder) in …
siegfriedpammer May 6, 2026
d28ecf1
Add TransformAsyncHelpersAwaitToAwait to EarlyExpressionTransforms.
siegfriedpammer May 17, 2026
255e73f
Reverse runtime-async lowering of try-finally and try-catch with await.
siegfriedpammer May 7, 2026
58baac0
Normalize runtime-async catch filters to recover catch-with-when.
siegfriedpammer May 7, 2026
7c39e9e
Reduce multi-handler runtime-async try-catch back to a multi-handler …
siegfriedpammer May 7, 2026
1bfa3b8
Reduce runtime-async manual await pattern (non-Task awaitables) to Aw…
siegfriedpammer May 7, 2026
b29dd71
Recover the this-copy that runtime-async inserts at the entry of stru…
siegfriedpammer May 7, 2026
c107c5a
Reverse runtime-async flag-based early-return across a try-finally.
siegfriedpammer May 7, 2026
fadcb35
Cover ConfigureAwait and [MethodImpl(NoInlining)] in the Async pretty…
siegfriedpammer May 8, 2026
8059fde
Reverse runtime-async exception lowering for try-catch nested inside …
siegfriedpammer May 20, 2026
8a03ee2
Reverse runtime-async try-finally where the try body always throws.
siegfriedpammer May 20, 2026
8e2e48f
Generalise runtime-async flag-based early-return for nested try-finally.
siegfriedpammer May 20, 2026
a9a43f9
Reverse runtime-async multi-handler try-catch dispatched via an if-ch…
siegfriedpammer May 20, 2026
391bf28
Drop the per-call predicate from FindFlagInitStore.
siegfriedpammer May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public enum CompilerOptions
CheckForOverflowUnderflow = 0x20000,
ProcessXmlDoc = 0x40000,
UseRoslyn4_14_0 = 0x80000,
EnableRuntimeAsync = 0x100000,
UseMcsMask = UseMcs2_6_4 | UseMcs5_23,
UseRoslynMask = UseRoslyn1_3_2 | UseRoslyn2_10_0 | UseRoslyn3_11_0 | UseRoslyn4_14_0 | UseRoslynLatest
}
Expand Down Expand Up @@ -500,6 +501,10 @@ public static List<string> GetPreprocessorSymbols(CompilerOptions flags)
preprocessorSymbols.Add("LEGACY_CSC");
preprocessorSymbols.Add("LEGACY_VBC");
}
if (flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
preprocessorSymbols.Add("RUNTIMEASYNC");
}
return preprocessorSymbols;
}

Expand Down Expand Up @@ -605,13 +610,18 @@ public static async Task<CompilerResults> CompileCSharp(string sourceFileName, C
if (roslynVersion != "legacy")
{
otherOptions += "/shared ";
if (!targetNet40 && Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion)).Major > 2)
var version = Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion));
if (!targetNet40 && version.Major > 2)
{
if (flags.HasFlag(CompilerOptions.NullableEnable))
otherOptions += "/nullable+ ";
else
otherOptions += "/nullable- ";
}
if (!targetNet40 && roslynVersion == roslynLatestVersion && flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
otherOptions += "/features:runtime-async=on ";
Comment thread
siegfriedpammer marked this conversation as resolved.
}
Comment on lines +621 to +624
}

if (flags.HasFlag(CompilerOptions.Library))
Expand Down Expand Up @@ -842,6 +852,8 @@ internal static string GetSuffix(CompilerOptions cscOptions)
suffix += ".mcs2";
if ((cscOptions & CompilerOptions.UseMcs5_23) != 0)
suffix += ".mcs5";
if ((cscOptions & CompilerOptions.EnableRuntimeAsync) != 0)
suffix += ".runtimeasync";
return suffix;
}

Expand Down
40 changes: 40 additions & 0 deletions ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,46 @@ public async Task CustomTaskType([ValueSource(nameof(roslyn2OrNewerOptions))] Co
await RunForLibrary(cscOptions: cscOptions);
}

[Test]
public async Task RuntimeAsync([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("Async", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncForeach([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("AsyncForeach", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview | CompilerOptions.GeneratePdb);
}

[Test]
public async Task RuntimeAsyncMain([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await Run("AsyncMain", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncStreams([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("AsyncStreams", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncUsing([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(
"AsyncUsing",
cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview,
configureDecompiler: settings => { settings.UseEnhancedUsing = false; }
);
}

[Test]
public async Task RuntimeAsyncCustomTaskType([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("CustomTaskType", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task NullableRefTypes([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
Expand Down
145 changes: 145 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ public async Task SimpleVoidTaskMethod()
Console.WriteLine("After");
}

[MethodImpl(MethodImplOptions.NoInlining)]
public async Task NoInliningTaskMethod()
{
await Task.Yield();
}

public async Task TaskMethodWithoutAwait()
{
Console.WriteLine("No Await");
Expand Down Expand Up @@ -115,6 +121,24 @@ public async void AwaitInLoopCondition()
}
}

public async Task AwaitConfigureAwaitFalse(Task<int> task)
{
#if ROSLYN2
Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false));
#else
Console.WriteLine(await task.ConfigureAwait(false));
#endif
}

public async Task<int> AwaitConfigureAwaitMixed(Task<int> task1, Task<int> task2)
{
#if ROSLYN2
return await task1.ConfigureAwait(continueOnCapturedContext: false) + await task2.ConfigureAwait(continueOnCapturedContext: true);
#else
return await task1.ConfigureAwait(false) + await task2.ConfigureAwait(true);
#endif
}

#if CS60
public async Task AwaitInCatch(bool b, Task<int> task1, Task<int> task2)
{
Expand Down Expand Up @@ -359,6 +383,127 @@ public async Task<object> Issue2436()
}
return new object();
}

public async Task TryCatchFinallyAllAwait()
{
try
{
await Task.CompletedTask;
Console.WriteLine("try");
}
catch (Exception)
{
await Task.CompletedTask;
Console.WriteLine("catch");
}
finally
{
await Task.CompletedTask;
Console.WriteLine("finally");
}
}

public async Task ThrowInsideTryFinally()
{
try
{
throw new InvalidOperationException();
}
finally
{
await Task.Yield();
}
}

public async Task HeterogeneousMultiCatch1()
{
try
{
await Task.Yield();
}
catch (InvalidOperationException ex)
{
await Task.Yield();
Console.WriteLine(ex.Message);
}
catch (ArgumentException ex2)
{
await Task.Yield();
Console.WriteLine(ex2.Message);
}
}

public async Task HeterogeneousMultiCatch2()
{
try
{
await Task.Yield();
}
catch (InvalidOperationException ex)
{
await Task.Yield();
Console.WriteLine(ex.Message);
}
catch
{
await Task.Yield();
Console.WriteLine("other");
}
}

public async Task HeterogeneousMultiCatch3()
{
try
{
await Task.Yield();
}
catch (InvalidOperationException ex)
{
await Task.Yield();
Console.WriteLine(ex.Message);
}
catch (Exception)
{
await Task.Yield();
throw;
}
}
#if RUNTIMEASYNC
// The state-machine async lowering doesn't recognize return-from-try-with-await-in-finally
// and decompiles these as `int result; try { ... } finally { ... } return result;`. The
// runtime-async exception rewrite recovers the source-level form. Gate these tests so the
// (state-machine) Async test doesn't run them against the more aggressive output.
public async Task<int> ReturnFromTryFinally()
{
try
{
return 42;
}
finally
{
await Task.CompletedTask;
}
}

public async Task<int> ReturnFromInsideNestedTryFinally()
{
try
{
try
{
return 42;
}
finally
{
await Task.CompletedTask;
}
}
finally
{
await Task.CompletedTask;
}
}
#endif
#endif

public static async Task<int> GetIntegerSumAsync(IEnumerable<int> items)
Expand Down
3 changes: 2 additions & 1 deletion ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public enum LanguageVersion
CSharp12_0 = 1200,
CSharp13_0 = 1300,
CSharp14_0 = 1400,
Preview = 1400,
CSharp15_0 = 1500,
Preview = 1500,
Latest = 0x7FFFFFFF
}
}
28 changes: 26 additions & 2 deletions ICSharpCode.Decompiler/DecompilerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,16 @@ public void SetLanguageVersion(CSharp.LanguageVersion languageVersion)
extensionMembers = false;
firstClassSpanTypes = false;
}
if (languageVersion < CSharp.LanguageVersion.CSharp15_0)
{
runtimeAsync = false;
}
}

public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
if (runtimeAsync)
return CSharp.LanguageVersion.CSharp15_0;
Comment on lines +188 to +189
Comment on lines +188 to +189
if (extensionMembers || firstClassSpanTypes)
return CSharp.LanguageVersion.CSharp14_0;
if (paramsCollections)
Expand Down Expand Up @@ -2167,7 +2173,7 @@ public bool InlineArrays {
/// <summary>
/// Gets/Sets whether C# 14.0 extension members should be transformed.
/// </summary>
[Category("C# 14.0 / VS 202x.yy")]
[Category("C# 14.0 / VS 2026")]
[Description("DecompilerSettings.ExtensionMembers")]
public bool ExtensionMembers {
get { return extensionMembers; }
Expand All @@ -2185,7 +2191,7 @@ public bool ExtensionMembers {
/// <summary>
/// Gets/Sets whether (ReadOnly)Span&lt;T&gt; should be treated like built-in types.
/// </summary>
[Category("C# 14.0 / VS 202x.yy")]
[Category("C# 14.0 / VS 2026")]
[Description("DecompilerSettings.FirstClassSpanTypes")]
public bool FirstClassSpanTypes {
get { return firstClassSpanTypes; }
Expand All @@ -2198,6 +2204,24 @@ public bool FirstClassSpanTypes {
}
}

bool runtimeAsync = true;

/// <summary>
/// Gets/Sets whether runtime async should be used.
/// </summary>
[Category("C# 15.0 / VS 202x.yy")]
[Description("DecompilerSettings.RuntimeAsync")]
Comment on lines +2212 to +2213
public bool RuntimeAsync {
get { return runtimeAsync; }
set {
if (runtimeAsync != value)
{
runtimeAsync = value;
OnPropertyChanged();
}
}
}

bool separateLocalVariableDeclarations = false;

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
<Compile Include="Disassembler\DisassemblerSignatureTypeProvider.cs" />
<Compile Include="Documentation\XmlDocumentationElement.cs" />
<Compile Include="IL\ControlFlow\AwaitInFinallyTransform.cs" />
<Compile Include="IL\ControlFlow\RuntimeAsyncExceptionRewriteTransform.cs" />
<Compile Include="IL\ControlFlow\RuntimeAsyncManualAwaitTransform.cs" />
<Compile Include="IL\Transforms\InterpolatedStringTransform.cs" />
<Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" />
<Compile Include="IL\Transforms\LdLocaDupInitObjTransform.cs" />
Expand Down
Loading
Loading