Browse Source

Merge pull request #3731 from siegfriedpammer/runtime-async

master
Christoph Wille 3 days ago committed by GitHub
parent
commit
98ee6c3b84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
  2. 40
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 145
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs
  4. 3
      ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
  5. 28
      ICSharpCode.Decompiler/DecompilerSettings.cs
  6. 2
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  7. 59
      ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
  8. 136
      ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs
  9. 1416
      ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs
  10. 177
      ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs
  11. 16
      ICSharpCode.Decompiler/IL/ILReader.cs
  12. 33
      ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs
  13. 1
      ICSharpCode.Decompiler/SRMHacks.cs
  14. 7
      ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs
  15. 4
      ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs
  16. 22
      ICSharpCode.Decompiler/TypeSystem/TaskType.cs
  17. 3
      ILSpy/Languages/CSharpLanguage.cs
  18. 11
      ILSpy/Properties/Resources.Designer.cs
  19. 3
      ILSpy/Properties/Resources.resx

14
ICSharpCode.Decompiler.Tests/Helpers/Tester.cs

@ -73,6 +73,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers @@ -73,6 +73,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers
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
}
@ -500,6 +501,10 @@ namespace System.Runtime.CompilerServices @@ -500,6 +501,10 @@ namespace System.Runtime.CompilerServices
preprocessorSymbols.Add("LEGACY_CSC");
preprocessorSymbols.Add("LEGACY_VBC");
}
if (flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
preprocessorSymbols.Add("RUNTIMEASYNC");
}
return preprocessorSymbols;
}
@ -605,13 +610,18 @@ namespace System.Runtime.CompilerServices @@ -605,13 +610,18 @@ namespace System.Runtime.CompilerServices
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 ";
}
}
if (flags.HasFlag(CompilerOptions.Library))
@ -842,6 +852,8 @@ namespace System.Runtime.CompilerServices @@ -842,6 +852,8 @@ namespace System.Runtime.CompilerServices
suffix += ".mcs2";
if ((cscOptions & CompilerOptions.UseMcs5_23) != 0)
suffix += ".mcs5";
if ((cscOptions & CompilerOptions.EnableRuntimeAsync) != 0)
suffix += ".runtimeasync";
return suffix;
}

40
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

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

145
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs

@ -74,6 +74,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -74,6 +74,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine("After");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public async Task NoInliningTaskMethod()
{
await Task.Yield();
}
public async Task TaskMethodWithoutAwait()
{
Console.WriteLine("No Await");
@ -115,6 +121,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -115,6 +121,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
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)
{
@ -359,6 +383,127 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -359,6 +383,127 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
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)

3
ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs

@ -37,7 +37,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -37,7 +37,8 @@ namespace ICSharpCode.Decompiler.CSharp
CSharp12_0 = 1200,
CSharp13_0 = 1300,
CSharp14_0 = 1400,
Preview = 1400,
CSharp15_0 = 1500,
Preview = 1500,
Latest = 0x7FFFFFFF
}
}

28
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -177,10 +177,16 @@ namespace ICSharpCode.Decompiler @@ -177,10 +177,16 @@ namespace ICSharpCode.Decompiler
extensionMembers = false;
firstClassSpanTypes = false;
}
if (languageVersion < CSharp.LanguageVersion.CSharp15_0)
{
runtimeAsync = false;
}
}
public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
if (runtimeAsync)
return CSharp.LanguageVersion.CSharp15_0;
if (extensionMembers || firstClassSpanTypes)
return CSharp.LanguageVersion.CSharp14_0;
if (paramsCollections)
@ -2167,7 +2173,7 @@ namespace ICSharpCode.Decompiler @@ -2167,7 +2173,7 @@ namespace ICSharpCode.Decompiler
/// <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; }
@ -2185,7 +2191,7 @@ namespace ICSharpCode.Decompiler @@ -2185,7 +2191,7 @@ namespace ICSharpCode.Decompiler
/// <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; }
@ -2198,6 +2204,24 @@ namespace ICSharpCode.Decompiler @@ -2198,6 +2204,24 @@ namespace ICSharpCode.Decompiler
}
}
bool runtimeAsync = true;
/// <summary>
/// Gets/Sets whether runtime async should be used.
/// </summary>
[Category("C# 15.0 / VS 202x.yy")]
[Description("DecompilerSettings.RuntimeAsync")]
public bool RuntimeAsync {
get { return runtimeAsync; }
set {
if (runtimeAsync != value)
{
runtimeAsync = value;
OnPropertyChanged();
}
}
}
bool separateLocalVariableDeclarations = false;
/// <summary>

2
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -149,6 +149,8 @@ @@ -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" />

59
ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs

@ -121,7 +121,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -121,7 +121,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
awaitDebugInfos.Clear();
moveNextLeaves.Clear();
if (!MatchTaskCreationPattern(function) && !MatchAsyncEnumeratorCreationPattern(function))
{
if (function.IsAsync && context.Settings.RuntimeAsync)
{
TranslateThisCopyAccess(function);
RuntimeAsyncManualAwaitTransform.Run(function, context);
RuntimeAsyncExceptionRewriteTransform.Run(function, context);
}
return;
}
try
{
AnalyzeMoveNext();
@ -174,6 +182,57 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -174,6 +182,57 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
function.AsyncDebugInfo = new AsyncDebugInfo(catchHandlerOffset, awaitDebugInfos.ToImmutableArray());
}
// Runtime-async analog of fieldToParameterMap's `<>4__this` capture: in a struct method,
// Roslyn lowers `this` references to a single ldobj at function entry stored into a local.
// Match the entry instruction `stloc V_X(ldobj T(ldloc this))` and rewrite every later
// `ldloc V_X` / `ldloca V_X` to go through the `this` parameter again, so AST emission
// renders accesses as plain `this.field` / `field` rather than `<copy>.field`.
static bool TranslateThisCopyAccess(ILFunction function)
{
// Only applies to struct methods — that's the only shape where Roslyn copies *this
// at entry to keep the managed ref off the async resume path.
if (function.Method?.DeclaringTypeDefinition?.Kind != TypeKind.Struct)
return false;
var thisParam = function.Variables.FirstOrDefault(v => v.Kind == VariableKind.Parameter && v.Index == -1);
if (thisParam == null)
return false;
if (thisParam.Type is not ByReferenceType thisRef)
return false;
if (function.Body is not BlockContainer body || body.Blocks.Count == 0)
return false;
var entry = body.EntryPoint;
if (entry.Instructions.Count == 0)
return false;
if (!entry.Instructions[0].MatchStLoc(out var copyVar, out var copyValue))
return false;
if (copyVar.Kind != VariableKind.Local)
return false;
if (copyVar.StoreInstructions.Count != 1)
return false;
if (copyValue is not LdObj ldobj)
return false;
if (!ldobj.Target.MatchLdLoc(thisParam))
return false;
if (!copyVar.Type.Equals(ldobj.Type) || !thisRef.ElementType.Equals(ldobj.Type))
return false;
foreach (var ldloc in function.Descendants.OfType<LdLoc>().ToArray())
{
if (ldloc.Variable != copyVar)
continue;
ldloc.ReplaceWith(new LdObj(new LdLoc(thisParam), ldobj.Type).WithILRange(ldloc));
}
foreach (var ldloca in function.Descendants.OfType<LdLoca>().ToArray())
{
if (ldloca.Variable != copyVar)
continue;
ldloca.ReplaceWith(new LdLoc(thisParam).WithILRange(ldloca));
}
entry.Instructions.RemoveAt(0);
return true;
}
private void CleanUpBodyOfMoveNext(ILFunction function)
{
context.StepStartGroup("CleanUpBodyOfMoveNext", function);

136
ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs

@ -78,31 +78,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -78,31 +78,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// [stloc V_3(ldloc E_100) - copy exception variable to a temporary]
// stloc V_6(ldloc V_3) - store exception in 'global' object variable
// br IL_0075 - jump out of catch block to the head of the finallyBlock
var catchBlockEntry = catchBlockContainer.EntryPoint;
ILVariable objectVariable;
switch (catchBlockEntry.Instructions.Count)
if (!MatchObjectStoreCatchHandler(catchBlockContainer, exceptionVariable,
out var objectVariable, out var entryPointOfFinally))
{
case 2:
if (!catchBlockEntry.Instructions[0].MatchStLoc(out objectVariable, out var value))
continue;
if (!value.MatchLdLoc(exceptionVariable))
continue;
break;
case 3:
if (!catchBlockEntry.Instructions[0].MatchStLoc(out var temporaryVariable, out value))
continue;
if (!value.MatchLdLoc(exceptionVariable))
continue;
if (!catchBlockEntry.Instructions[1].MatchStLoc(out objectVariable, out value))
continue;
if (!value.MatchLdLoc(temporaryVariable))
continue;
break;
default:
continue;
}
if (!catchBlockEntry.Instructions[catchBlockEntry.Instructions.Count - 1].MatchBranch(out var entryPointOfFinally))
continue;
}
// globalCopyVar should only be used once, at the end of the finally-block
if (objectVariable.LoadCount != 1 || objectVariable.StoreCount > 2)
continue;
@ -136,7 +116,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -136,7 +116,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
tryCatch.ReplaceWith(new TryFinally(tryCatch.TryBlock, finallyContainer).WithILRange(tryCatch.TryBlock));
context.Step("Move blocks into finally", finallyContainer);
MoveDominatedBlocksToContainer(entryPointOfFinally, beforeExceptionCaptureBlock, cfg, finallyContainer);
MoveDominatedBlocksToContainer(entryPointOfFinally, beforeExceptionCaptureBlock, cfg, finallyContainer, context);
SimplifyEndOfFinally(context, objectVariable, beforeExceptionCaptureBlock, objectVariableCopy, finallyContainer);
@ -193,38 +173,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -193,38 +173,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
}
void MoveDominatedBlocksToContainer(Block newEntryPoint, Block endBlock, ControlFlowGraph graph,
BlockContainer targetContainer)
{
var node = graph.GetNode(newEntryPoint);
var endNode = endBlock == null ? null : graph.GetNode(endBlock);
MoveBlock(newEntryPoint, targetContainer);
foreach (var n in graph.cfg)
{
Block block = (Block)n.UserData;
if (node.Dominates(n))
{
if (endNode != null && endNode != n && endNode.Dominates(n))
continue;
if (block.Parent == targetContainer)
continue;
MoveBlock(block, targetContainer);
}
}
}
void MoveBlock(Block block, BlockContainer target)
{
context.Step($"Move {block.Label} to container at IL_{target.StartILOffset:x4}", target);
block.Remove();
target.Blocks.Add(block);
}
static void SimplifyEndOfFinally(ILTransformContext context, ILVariable objectVariable, Block beforeExceptionCaptureBlock, ILVariable objectVariableCopy, BlockContainer finallyContainer)
{
if (beforeExceptionCaptureBlock.Instructions.Count >= 3
@ -281,6 +229,82 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -281,6 +229,82 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return true;
}
/// <summary>
/// Matches a catch handler that stores its exception in an "object"-typed local and then
/// branches out of the catch:
/// [stloc tmp(ldloc exceptionVariable);]
/// stloc objectVariable(ldloc tmp_or_exceptionVariable)
/// br branchTarget
/// Used by the state-machine async lowering AND by the runtime-async try-finally lowering.
/// </summary>
internal static bool MatchObjectStoreCatchHandler(BlockContainer catchBlockContainer,
ILVariable exceptionVariable, out ILVariable objectVariable, out Block branchTarget)
{
objectVariable = null;
branchTarget = null;
var entry = catchBlockContainer.EntryPoint;
ILInstruction value;
switch (entry.Instructions.Count)
{
case 2:
if (!entry.Instructions[0].MatchStLoc(out objectVariable, out value))
return false;
if (!value.MatchLdLoc(exceptionVariable))
return false;
break;
case 3:
if (!entry.Instructions[0].MatchStLoc(out var temporaryVariable, out value))
return false;
if (!value.MatchLdLoc(exceptionVariable))
return false;
if (!entry.Instructions[1].MatchStLoc(out objectVariable, out value))
return false;
if (!value.MatchLdLoc(temporaryVariable))
return false;
break;
default:
return false;
}
return entry.Instructions[entry.Instructions.Count - 1].MatchBranch(out branchTarget);
}
/// <summary>
/// Move <paramref name="newEntryPoint"/> and every block it dominates (excluding any block
/// dominated by <paramref name="endBlock"/> other than <paramref name="endBlock"/> itself)
/// from their current container into <paramref name="targetContainer"/>.
/// </summary>
internal static void MoveDominatedBlocksToContainer(Block newEntryPoint, Block endBlock,
ControlFlowGraph graph, BlockContainer targetContainer, ILTransformContext context)
{
var node = graph.GetNode(newEntryPoint);
var endNode = endBlock == null ? null : graph.GetNode(endBlock);
MoveBlock(newEntryPoint, targetContainer, context);
foreach (var n in graph.cfg)
{
Block block = (Block)n.UserData;
if (node.Dominates(n))
{
if (endNode != null && endNode != n && endNode.Dominates(n))
continue;
if (block.Parent == targetContainer)
continue;
MoveBlock(block, targetContainer, context);
}
}
}
static void MoveBlock(Block block, BlockContainer target, ILTransformContext context)
{
context.Step($"Move {block.Label} to container at IL_{target.StartILOffset:x4}", target);
block.Remove();
target.Blocks.Add(block);
}
static (Block, Block, ILVariable) FindBlockAfterFinally(ILTransformContext context, Block block, ILVariable objectVariable)
{
// Block IL_0327 (incoming: 2) {

1416
ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

File diff suppressed because it is too large Load Diff

177
ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs

@ -0,0 +1,177 @@ @@ -0,0 +1,177 @@
// 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.
using System.Linq;
using ICSharpCode.Decompiler.IL.Transforms;
namespace ICSharpCode.Decompiler.IL.ControlFlow
{
/// <summary>
/// Collapses the runtime-async manual await pattern (GetAwaiter / get_IsCompleted /
/// AsyncHelpers.UnsafeAwaitAwaiter / GetResult across three blocks) into a single IL
/// Await instruction, matching what the state-machine async pipeline emits.
/// </summary>
static class RuntimeAsyncManualAwaitTransform
{
public static void Run(ILFunction function, ILTransformContext context)
{
bool changed = false;
foreach (var block in function.Descendants.OfType<Block>().ToArray())
{
if (DetectRuntimeAsyncManualAwait(block, context))
changed = true;
}
if (changed)
{
foreach (var c in function.Body.Descendants.OfType<BlockContainer>())
c.SortBlocks(deleteUnreachableBlocks: true);
}
}
// Block X (head) {
// [stloc awaitable(<value>);] (optional: when GetAwaiter takes an address)
// stloc awaiter(call GetAwaiter(<expr>))
// if (call get_IsCompleted(ldloca awaiter)) br completedBlock
// br pauseBlock
// }
// Block pauseBlock {
// call AsyncHelpers.UnsafeAwaitAwaiter[<T>](ldloc awaiter)
// br completedBlock
// }
// Block completedBlock {
// call GetResult(ldloca awaiter) (possibly inlined into a containing expression)
// ...
// }
// =>
// Block X { ...; br completedBlock }
// Block completedBlock { <Await(value)>; ... }
static bool DetectRuntimeAsyncManualAwait(Block block, ILTransformContext context)
{
if (block.Instructions.Count < 3)
return false;
if (!block.Instructions[^3].MatchStLoc(out var awaiterVar, out var awaiterValue))
return false;
if (awaiterValue is not CallInstruction getAwaiterCall)
return false;
if (getAwaiterCall.Method.Name != "GetAwaiter" || getAwaiterCall.Arguments.Count != 1)
return false;
if (block.Instructions[^2] is not IfInstruction ifInst)
return false;
var condition = ifInst.Condition;
if (condition.MatchLogicNot(out var negated))
condition = negated;
if (condition is not CallInstruction isCompletedCall)
return false;
if (isCompletedCall.Method.Name != "get_IsCompleted" || isCompletedCall.Arguments.Count != 1)
return false;
if (!isCompletedCall.Arguments[0].MatchLdLoca(awaiterVar))
return false;
Block completedBlock, pauseBlock;
if (ifInst.TrueInst.MatchBranch(out var trueBranchTarget) && block.Instructions[^1].MatchBranch(out var falseBranchTarget))
{
if (negated != null)
{
// if (!IsCompleted) br pauseBlock; br completedBlock — flipped
pauseBlock = trueBranchTarget;
completedBlock = falseBranchTarget;
}
else
{
// if (IsCompleted) br completedBlock; br pauseBlock — canonical
completedBlock = trueBranchTarget;
pauseBlock = falseBranchTarget;
}
}
else
{
return false;
}
// Block pauseBlock { call AsyncHelpers.UnsafeAwaitAwaiter(ldloc awaiter); br completedBlock }
if (pauseBlock.Instructions.Count != 2)
return false;
if (pauseBlock.Instructions[0] is not Call pauseCall)
return false;
if (!EarlyExpressionTransforms.IsAsyncHelpersMethod(pauseCall.Method, "UnsafeAwaitAwaiter") || pauseCall.Arguments.Count != 1)
return false;
if (!pauseCall.Arguments[0].MatchLdLoc(awaiterVar))
return false;
if (!pauseBlock.Instructions[1].MatchBranch(out var pauseTail) || pauseTail != completedBlock)
return false;
// completedBlock starts with: GetResult(ldloca awaiter), possibly inlined.
if (completedBlock.Instructions.Count < 1)
return false;
var getResultCall = ILInlining.FindFirstInlinedCall(completedBlock.Instructions[0]);
if (getResultCall == null)
return false;
if (getResultCall.Method.Name != "GetResult" || getResultCall.Arguments.Count != 1)
return false;
if (!getResultCall.Arguments[0].MatchLdLoca(awaiterVar))
return false;
// Determine the awaitable expression: the original GetAwaiter argument (often `ldloca tmp`)
// or, if `tmp` is a fresh temporary set immediately above, the value it was set to.
ILInstruction awaitable;
bool removeTemporaryStore = false;
if (getAwaiterCall.Arguments[0].MatchLdLoca(out var tmpVar)
&& block.Instructions.Count >= 4
&& block.Instructions[^4].MatchStLoc(tmpVar, out var tmpValue)
&& tmpVar.StoreCount == 1
&& tmpVar.LoadCount == 0
&& tmpVar.AddressCount == 1)
{
awaitable = tmpValue;
removeTemporaryStore = true;
}
else
{
awaitable = getAwaiterCall.Arguments[0];
}
context.Step("Reduce manual await pattern to Await", block);
// Capture IL ranges before we remove the suspend machinery. The new Await stands in
// for the whole pattern (temp store + GetAwaiter + IsCompleted check + UnsafeAwaitAwaiter
// + GetResult), and the new `br completedBlock` replaces the original `br pauseBlock`.
var oldTailBranch = block.Instructions[^1];
var awaitInst = new Await(awaitable.Clone()).WithILRange(getResultCall);
if (removeTemporaryStore)
awaitInst.AddILRange(block.Instructions[^4]);
awaitInst.AddILRange(block.Instructions[^3]);
awaitInst.AddILRange(block.Instructions[^2]);
foreach (var inst in pauseBlock.Instructions)
awaitInst.AddILRange(inst);
awaitInst.GetAwaiterMethod = getAwaiterCall.Method;
awaitInst.GetResultMethod = getResultCall.Method;
// Remove the trailing 3 (or 4) instructions of the head block; replace with `br completedBlock`.
int removeCount = removeTemporaryStore ? 4 : 3;
block.Instructions.RemoveRange(block.Instructions.Count - removeCount, removeCount);
block.Instructions.Add(new Branch(completedBlock).WithILRange(oldTailBranch));
getResultCall.ReplaceWith(awaitInst);
return true;
}
}
}

16
ICSharpCode.Decompiler/IL/ILReader.cs

@ -159,6 +159,7 @@ namespace ICSharpCode.Decompiler.IL @@ -159,6 +159,7 @@ namespace ICSharpCode.Decompiler.IL
BitSet isBranchTarget = null!;
BlockContainer mainContainer = null!;
int currentInstructionStart;
bool isRuntimeAsync;
Dictionary<int, ImportedBlock> blocksByOffset = new Dictionary<int, ImportedBlock>();
Queue<ImportedBlock> importQueue = new Queue<ImportedBlock>();
@ -172,6 +173,8 @@ namespace ICSharpCode.Decompiler.IL @@ -172,6 +173,8 @@ namespace ICSharpCode.Decompiler.IL
if (methodDefinitionHandle.IsNil)
throw new ArgumentException("methodDefinitionHandle.IsNil");
this.method = module.GetDefinition(methodDefinitionHandle);
this.isRuntimeAsync = (module.TypeSystemOptions & TypeSystemOptions.RuntimeAsync) != 0
&& (module.metadata.GetMethodDefinition(methodDefinitionHandle).ImplAttributes & SRMExtensions.MethodImplAsync) != 0;
if (genericContext.ClassTypeParameters == null && genericContext.MethodTypeParameters == null)
{
// no generic context specified: use the method's own type parameters
@ -187,7 +190,14 @@ namespace ICSharpCode.Decompiler.IL @@ -187,7 +190,14 @@ namespace ICSharpCode.Decompiler.IL
this.reader = body.GetILReader();
this.currentStack = ImmutableStack<ILVariable>.Empty;
this.expressionStack.Clear();
this.methodReturnStackType = method.ReturnType.GetStackType();
if (isRuntimeAsync)
{
this.methodReturnStackType = TaskType.UnpackAnyTask(compilation, method.ReturnType).GetStackType();
}
else
{
this.methodReturnStackType = method.ReturnType.GetStackType();
}
InitParameterVariables();
localVariables = InitLocalVariables();
foreach (var v in localVariables)
@ -715,6 +725,10 @@ namespace ICSharpCode.Decompiler.IL @@ -715,6 +725,10 @@ namespace ICSharpCode.Decompiler.IL
var blockBuilder = new BlockBuilder(body, variableByExceptionHandler, compilation);
blockBuilder.CreateBlocks(mainContainer, blocksByOffset.Values.Select(ib => ib.Block), cancellationToken);
var function = new ILFunction(this.method, body.GetCodeSize(), this.genericContext, mainContainer, kind);
if (isRuntimeAsync)
{
function.AsyncReturnType = TaskType.UnpackAnyTask(compilation, this.method.ReturnType);
}
function.Variables.AddRange(parameterVariables);
function.Variables.AddRange(localVariables);
function.LocalVariableSignatureLength = localVariables.Length;

33
ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs

@ -173,6 +173,39 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -173,6 +173,39 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
protected internal override void VisitCall(Call inst)
{
base.VisitCall(inst);
TransformAsyncHelpersAwaitToAwait(inst, context);
}
// runtime-async lowering emits `call AsyncHelpers.Await(value)` in place of the IL Await
// instruction. Convert it back so downstream transforms (UsingTransform's MatchDisposeBlock
// and friends pattern-match on Await via UnwrapAwait) see the canonical shape that the
// state-machine async pipeline also produces. Gate on the current function actually being
// a runtime-async method — ILReader sets IsAsync iff the MethodImpl.Async bit is present —
// so a non-async method that calls the helper directly (legal, just unusual) doesn't get
// rewritten into IL that the surrounding non-async control flow can't represent.
internal static bool TransformAsyncHelpersAwaitToAwait(Call inst, ILTransformContext context)
{
if (!context.Settings.RuntimeAsync || !context.Function.IsAsync)
return false;
if (!inst.Method.IsStatic || inst.Arguments.Count != 1 || !IsAsyncHelpersMethod(inst.Method, "Await"))
return false;
context.Step("call AsyncHelpers.Await(value) => await(value)", inst);
var awaitInst = new Await(inst.Arguments[0]).WithILRange(inst);
awaitInst.GetAwaiterMethod = null;
awaitInst.GetResultMethod = inst.Method;
inst.ReplaceWith(awaitInst);
return true;
}
internal static bool IsAsyncHelpersMethod(IMethod method, string name)
{
return method.Name == name
&& method.DeclaringType?.FullName == "System.Runtime.CompilerServices.AsyncHelpers";
}
protected internal override void VisitNewObj(NewObj inst)
{
if (TransformDecimalCtorToConstant(inst, out LdcDecimal decimalConstant))

1
ICSharpCode.Decompiler/SRMHacks.cs

@ -15,6 +15,7 @@ namespace ICSharpCode.Decompiler @@ -15,6 +15,7 @@ namespace ICSharpCode.Decompiler
public static partial class SRMExtensions
{
internal const GenericParameterAttributes AllowByRefLike = (GenericParameterAttributes)0x0020;
internal const MethodImplAttributes MethodImplAsync = (MethodImplAttributes)0x2000;
public static ImmutableArray<MethodImplementationHandle> GetMethodImplementations(
this MethodDefinitionHandle handle, MetadataReader reader)

7
ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs

@ -154,12 +154,17 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -154,12 +154,17 @@ namespace ICSharpCode.Decompiler.TypeSystem
/// </summary>
ExtensionMembers = 0x80000,
/// <summary>
/// If this option is active, methods with the MethodImplAttribute(MethodImplOptions.Async) are treated as async methods.
/// </summary>
RuntimeAsync = 0x100000,
/// <summary>
/// Default settings: typical options for the decompiler, with all C# language features enabled.
/// </summary>
Default = Dynamic | Tuple | ExtensionMethods | DecimalConstants | ReadOnlyStructsAndParameters
| RefStructs | UnmanagedConstraints | NullabilityAnnotations | ReadOnlyMethods
| NativeIntegers | FunctionPointers | ScopedRef | NativeIntegersWithoutAttribute
| RefReadOnlyParameters | ParamsCollections | FirstClassSpanTypes | ExtensionMembers
| RuntimeAsync
}
/// <summary>
@ -207,6 +212,8 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -207,6 +212,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
typeSystemOptions |= TypeSystemOptions.FirstClassSpanTypes;
if (settings.ExtensionMembers)
typeSystemOptions |= TypeSystemOptions.ExtensionMembers;
if (settings.RuntimeAsync)
typeSystemOptions |= TypeSystemOptions.RuntimeAsync;
return typeSystemOptions;
}

4
ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs

@ -350,6 +350,10 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -350,6 +350,10 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
var metadata = module.metadata;
var def = metadata.GetMethodDefinition(handle);
MethodImplAttributes implAttributes = def.ImplAttributes & ~MethodImplAttributes.CodeTypeMask;
if ((module.TypeSystemOptions & TypeSystemOptions.RuntimeAsync) != 0)
{
implAttributes &= ~SRMExtensions.MethodImplAsync;
}
int methodCodeType = (int)(def.ImplAttributes & MethodImplAttributes.CodeTypeMask);
#region DllImportAttribute

22
ICSharpCode.Decompiler/TypeSystem/TaskType.cs

@ -40,6 +40,28 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -40,6 +40,28 @@ namespace ICSharpCode.Decompiler.TypeSystem
return type.TypeArguments[0];
}
/// <summary>
/// Gets the element type of any task-like type (Task, Task&lt;T&gt;, ValueTask,
/// ValueTask&lt;T&gt;, or any [AsyncMethodBuilder]-attributed custom task type).
/// Returns void for non-generic task-likes. Any other type is returned unmodified.
/// </summary>
public static IType UnpackAnyTask(ICompilation compilation, IType type)
{
if (IsTask(type))
{
return type.TypeParameterCount == 0
? compilation.FindType(KnownTypeCode.Void)
: type.TypeArguments[0];
}
if (IsCustomTask(type, out _))
{
return type.TypeParameterCount == 0
? compilation.FindType(KnownTypeCode.Void)
: type.TypeArguments[0];
}
return type;
}
/// <summary>
/// Gets whether the specified type is Task or Task&lt;T&gt;.
/// </summary>

3
ILSpy/Languages/CSharpLanguage.cs

@ -24,8 +24,6 @@ using System.Linq; @@ -24,8 +24,6 @@ using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Windows;
using System.Windows.Controls;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.Decompiler;
@ -119,6 +117,7 @@ namespace ICSharpCode.ILSpy @@ -119,6 +117,7 @@ namespace ICSharpCode.ILSpy
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp12_0.ToString(), "C# 12.0 / VS 2022.8"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp13_0.ToString(), "C# 13.0 / VS 2022.12"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp14_0.ToString(), "C# 14.0 / VS 2026"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp15_0.ToString(), "C# 15.0 / VS 202x.yy"),
};
}
return versions;

11
ILSpy/Properties/Resources.Designer.cs generated

@ -1360,7 +1360,16 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1360,7 +1360,16 @@ namespace ICSharpCode.ILSpy.Properties {
return ResourceManager.GetString("DecompilerSettings.RequiredMembers", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Decompile runtime-async (System.Runtime.CompilerServices.AsyncHelpers) methods.
/// </summary>
public static string DecompilerSettings_RuntimeAsync {
get {
return ResourceManager.GetString("DecompilerSettings.RuntimeAsync", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;scoped&apos; lifetime annotation.
/// </summary>

3
ILSpy/Properties/Resources.resx

@ -474,6 +474,9 @@ Are you sure you want to continue?</value> @@ -474,6 +474,9 @@ Are you sure you want to continue?</value>
<data name="DecompilerSettings.RequiredMembers" xml:space="preserve">
<value>Required members</value>
</data>
<data name="DecompilerSettings.RuntimeAsync" xml:space="preserve">
<value>Decompile runtime-async (System.Runtime.CompilerServices.AsyncHelpers) methods</value>
</data>
<data name="DecompilerSettings.ScopedRef" xml:space="preserve">
<value>'scoped' lifetime annotation</value>
</data>

Loading…
Cancel
Save