diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index 3c88f4a22a..22714e85a0 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -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 } @@ -605,13 +606,18 @@ public static async Task 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 "; + } } if (flags.HasFlag(CompilerOptions.Library)) @@ -842,6 +848,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; } diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 8810773c6f..a57606a47b 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -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); + } + + [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) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs index 326a9c57fc..fb21be0f51 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs @@ -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"); @@ -115,6 +121,24 @@ public async void AwaitInLoopCondition() } } + public async Task AwaitConfigureAwaitFalse(Task task) + { +#if ROSLYN2 + Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false)); +#else + Console.WriteLine(await task.ConfigureAwait(false)); +#endif + } + + public async Task AwaitConfigureAwaitMixed(Task task1, Task 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 task1, Task task2) { diff --git a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs index 07e76b0c92..66c4cdb578 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs @@ -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 } } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 8853717ab0..e9aeefb733 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -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; if (extensionMembers || firstClassSpanTypes) return CSharp.LanguageVersion.CSharp14_0; if (paramsCollections) @@ -2167,7 +2173,7 @@ public bool InlineArrays { /// /// Gets/Sets whether C# 14.0 extension members should be transformed. /// - [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 @@ public bool ExtensionMembers { /// /// Gets/Sets whether (ReadOnly)Span<T> should be treated like built-in types. /// - [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 @@ public bool FirstClassSpanTypes { } } + bool runtimeAsync = true; + + /// + /// Gets/Sets whether runtime async should be used. + /// + [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; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index aea2cfe174..c421fefc20 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -149,6 +149,8 @@ + + diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs index eb7d82c94d..fe694343fe 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -121,7 +121,15 @@ public void Run(ILFunction function, ILTransformContext context) 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 @@ public void Run(ILFunction function, ILTransformContext context) 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 `.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().ToArray()) + { + if (ldloc.Variable != copyVar) + continue; + ldloc.ReplaceWith(new LdObj(new LdLoc(thisParam), ldobj.Type).WithILRange(ldloc)); + } + foreach (var ldloca in function.Descendants.OfType().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); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs index 197a5ae660..21d5e2fbff 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs @@ -78,31 +78,11 @@ public static void Run(ILFunction function, ILTransformContext context) // [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 @@ public static void Run(ILFunction function, ILTransformContext context) 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 @@ public static void Run(ILFunction function, ILTransformContext context) } } - 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 @@ private static bool ValidateStateVariable(ILVariable stateVariable, StLoc initia return true; } + /// + /// 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. + /// + 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); + } + + /// + /// Move and every block it dominates (excluding any block + /// dominated by other than itself) + /// from their current container into . + /// + 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) { diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs new file mode 100644 index 0000000000..d1915add72 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs @@ -0,0 +1,1156 @@ +// 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; +using System.Collections.Generic; +using System.Linq; + +using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL.ControlFlow +{ + /// + /// Reverses Roslyn's runtime-async lowering of try-catch-with-await and + /// try-finally-with-await: + /// + /// try-catch with await is lowered to a flag-flat shape where the catch handler stores + /// a captured object plus a "did the catch fire" int flag, and the original catch body + /// runs after the protected region inside an if (flag == 1) guard. + /// + /// try-finally with await is lowered to a try-catch[object] whose handler stores the + /// exception, followed by the original finally body inlined, followed by a + /// if (obj != null) ExceptionDispatchInfo.Capture((obj as Exception) ?? throw obj).Throw() + /// rethrow idiom. + /// + /// This transform reverses both shapes so that the surrounding pipeline sees ordinary + /// TryCatch / TryFinally instructions. + /// + static class RuntimeAsyncExceptionRewriteTransform + { + public static void Run(ILFunction function, ILTransformContext context) + { + if (!context.Settings.AwaitInCatchFinally) + return; + + bool changed = false; + + // Pre-pass: normalize runtime-async catch filters that wrap a type-test + obj-store. + // `catch (T ex) when (filter)` is lowered to `catch object when ({ isinst T; obj=ex; })`, + // even when T is `object` (i.e. the source is `catch when (filter)`). After this pre-pass the + // catch handler looks structurally identical to a non-filter catch, so the body matcher in + // TryRewriteTryCatch can run unchanged. + // + // The captured obj local is shared across every catch handler that opts into the runtime-async + // rethrow protocol, so per-handler filter normalization must NOT do a function-wide remap of + // reads of obj — that would tag every reference (in both this handler's own dispatch idiom and + // every other handler's dispatch idiom) with the same handler variable. Instead, record the + // obj variable per handler and let TryRewriteTryCatch / the multi-handler transform remap it + // scoped to the moved catch body. + var filterObjByHandler = new Dictionary(); + foreach (var handler in function.Descendants.OfType().ToArray()) + { + if (NormalizeRuntimeAsyncFilter(handler, function, filterObjByHandler, context)) + changed = true; + } + + foreach (var tryCatch in function.Descendants.OfType().ToArray()) + { + if (tryCatch.Parent is not Block parentBlock) + continue; + if (parentBlock.Parent is not BlockContainer container) + continue; + if (tryCatch.ChildIndex != parentBlock.Instructions.Count - 1) + continue; + + if (tryCatch.Handlers.Count == 1) + { + var handler = tryCatch.Handlers[0]; + if (handler.Body is not BlockContainer handlerBody) + continue; + if (handlerBody.Blocks.Count != 1) + continue; + var handlerBlock = handlerBody.Blocks[0]; + + if (TryRewriteTryFinally(tryCatch, handler, handlerBlock, parentBlock, container, context)) + { + changed = true; + continue; + } + + if (TryRewriteTryCatch(tryCatch, handler, handlerBlock, parentBlock, container, filterObjByHandler, context)) + { + changed = true; + continue; + } + } + else if (tryCatch.Handlers.Count >= 2) + { + if (TryRewriteMultiHandlerTryCatch(tryCatch, parentBlock, container, filterObjByHandler, context)) + { + changed = true; + continue; + } + } + } + + // Recognize the "early return from inside a try via a flag local" pattern that runtime-async + // emits whenever a return statement crosses an enclosing try-finally with await. + foreach (var tryFinally in function.Descendants.OfType().ToArray()) + { + if (TryRewriteFlagBasedEarlyReturn(tryFinally, context)) + changed = true; + } + + if (changed) + { + foreach (var c in function.Body.Descendants.OfType()) + c.SortBlocks(deleteUnreachableBlocks: true); + } + } + + // Strip the runtime-async obj-store machinery from the entry of the filter's trueBody. + // After this runs, the filter has the same shape as a state-machine-async catch-when filter, + // and DetectCatchWhenConditionBlocks (the next EarlyILTransform) handles isinst recognition, + // retyping handler.Variable to the user's catch type, and propagating it through trivial + // stloc copies inside the handler. We only need to peel off the runtime-async-specific + // captured-object prefix. + // + // Shape we strip from trueBody: + // [stloc tmp2(ldloc tmpVar);] optional copy + // stloc obj(ldloc tmp_or_tmp2) + // [stloc typedEx(castclass T'(ldloc obj));] optional, when source has a typed catch var + // where tmpVar is the isinst-result local set in the filter's entry block. The captured `obj` + // local is shared across every catch handler in the multi-handler form, so it must NOT be + // remapped function-wide here; we record it per handler and let the body rewriter scope the + // remap to each handler's catch body. + static bool NormalizeRuntimeAsyncFilter(TryCatchHandler handler, ILFunction function, + Dictionary filterObjByHandler, ILTransformContext context) + { + if (!handler.Variable.Type.IsKnownType(KnownTypeCode.Object)) + return false; + if (handler.Filter is not BlockContainer filterContainer) + return false; + + // Find the isinst-result temp via the standard catch-when entry shape: + // stloc tmp(isinst T(ldloc handlerVar)); if (tmp != null) br trueBody; br falseBody + var entry = filterContainer.EntryPoint; + if (entry.Instructions.Count != 3) + return false; + if (!entry.Instructions[0].MatchStLoc(out var tmpVar, out var tmpValue)) + return false; + if (!tmpValue.MatchIsInst(out var isinstArg, out _)) + return false; + if (!isinstArg.MatchLdLoc(handler.Variable)) + return false; + if (entry.Instructions[1] is not IfInstruction ifInst) + return false; + if (!ifInst.Condition.MatchCompNotEqualsNull(out var condArg) || !condArg.MatchLdLoc(tmpVar)) + return false; + if (ifInst.TrueInst is not Branch trueBranch) + return false; + var trueBody = trueBranch.TargetBlock; + int n = trueBody.Instructions.Count; + if (n < 1) + return false; + + // Recognize the prefix. + int prefix = 0; + ILVariable tmp2Var = null; + ILVariable objVar; + if (!trueBody.Instructions[0].MatchStLoc(out var first, out var firstValue)) + return false; + if (!firstValue.MatchLdLoc(tmpVar)) + return false; + if (n > 1 + && trueBody.Instructions[1].MatchStLoc(out var second, out var secondValue) + && secondValue.MatchLdLoc(first)) + { + tmp2Var = first; + objVar = second; + prefix = 2; + } + else + { + objVar = first; + prefix = 1; + } + + ILVariable typedExVar = null; + if (prefix < n + && trueBody.Instructions[prefix] is StLoc castStore + && castStore.Value is CastClass castClass + && castClass.Argument.MatchLdLoc(objVar)) + { + typedExVar = castStore.Variable; + prefix++; + } + + context.StepStartGroup("Strip runtime-async catch-filter prefix", handler); + + trueBody.Instructions.RemoveRange(0, prefix); + + // Remap reads of the per-handler synthesized variables (tmp2 / typedEx) to handler.Variable. + // Both are unique per handler so a function-wide remap is safe. + if (tmp2Var != null) + RemapVariableReads(function, tmp2Var, handler.Variable); + if (typedExVar != null) + RemapVariableReads(function, typedExVar, handler.Variable); + + // Optimized builds inline `castclass T(ldloc obj)` directly into the user filter expression + // instead of stashing it in a typedEx local. Remap obj reads within the filter only. + RemapVariableReads(handler.Filter, objVar, handler.Variable); + + // The obj variable is shared between handlers in the multi-handler form; the body rewriter + // remaps it scoped per handler. + filterObjByHandler[handler] = objVar; + + context.StepEndGroup(keepIfEmpty: true); + return true; + } + + static void RemapVariableReads(ILInstruction root, ILVariable from, ILVariable to) + { + foreach (var ldloc in root.Descendants.OfType().ToArray()) + { + if (ldloc.Variable != from) + continue; + // Drop redundant castclass to the new handler type that may now wrap the load. + if (ldloc.Parent is CastClass cc && cc.Type.Equals(to.Type)) + cc.ReplaceWith(new LdLoc(to).WithILRange(cc).WithILRange(ldloc)); + else + ldloc.ReplaceWith(new LdLoc(to).WithILRange(ldloc)); + } + foreach (var stloc in root.Descendants.OfType().ToArray()) + { + if (stloc.Variable == from && stloc.Parent is Block parent) + parent.Instructions.RemoveAt(stloc.ChildIndex); + } + } + + // stloc obj(ldnull) + // .try { ... ; br continuation } + // catch ex : object when (ldc.i4 1) { + // [stloc tmp(ldloc ex);] + // stloc obj(ldloc tmp_or_ex) + // br continuation + // } + // Block continuation { + // + // if (obj == null) leave outer + // + // } + // => + // .try { ... ; leave finallyContainer } + // finally { } + // Block continuation { + // leave outer + // } + static bool TryRewriteTryFinally(TryCatch tryCatch, TryCatchHandler handler, Block handlerBlock, + Block parentBlock, BlockContainer container, ILTransformContext context) + { + if (!handler.Variable.Type.IsKnownType(KnownTypeCode.Object)) + return false; + + // match catch body: [stloc tmp(ldloc ex);] stloc obj(ldloc tmp_or_ex); br continuation + // Reuse the helper from AwaitInFinallyTransform — same shape on both lowerings. + if (!AwaitInFinallyTransform.MatchObjectStoreCatchHandler((BlockContainer)handler.Body, + handler.Variable, out var objectVariable, out var continuation)) + { + return false; + } + if (!objectVariable.Type.IsKnownType(KnownTypeCode.Object)) + return false; + if (continuation.Parent != container) + return false; + + // Pre-try: somewhere among the instructions preceding the TryCatch we expect + // an `stloc obj(ldnull)`. Other (unrelated) stores may be interleaved. + var flagInitStore = FindFlagInitStore(parentBlock, tryCatch, + s => s.Variable == objectVariable && s.Value.MatchLdNull()); + if (flagInitStore == null) + return false; + + // Every outward exit of the try body must branch to `continuation`. The runtime-async + // lowering rewrites every return / fallthrough to `br continuation` and routes throws + // through the synthetic catch, so a Leave or a Branch to anything else means we're not + // looking at a lowered shape. We also require at least one such exit, to reject try + // bodies with no outward control flow at all. + bool seenExit = false; + foreach (var inst in tryCatch.TryBlock.Descendants.OfType()) + { + // Skip intra-tryBody control flow: inst.TargetContainer being tryBody itself or any + // container nested inside tryBody means control stays within tryBody. Only when the + // target container is a strict ancestor of tryBody does the instruction exit it. + if (inst.TargetContainer.IsDescendantOf(tryCatch.TryBlock)) + continue; + if (inst is Branch branch && branch.TargetBlock == continuation) + seenExit = true; + else + return false; + } + if (!seenExit) + return false; + + // Find the dispatch idiom at the end of the finally body. + // Pattern: a block ending with "if (obj == null) leave outer; br dispatchHead" + // where dispatchHead is the block containing the isinst Exception + ExceptionDispatchInfo idiom. + if (!FindFinallyDispatchExit(continuation, objectVariable, container, + out var finallyExitBlock, out var dispatchBlocks, out var afterFinallyExit)) + return false; + + context.StepStartGroup("Rewrite runtime-async try-finally", tryCatch); + + // Determine whether the if-true branch of the finally exit is a direct Leave-with-value + // or a Branch to a one-instruction leave block. In the latter case, that block stays + // in the outer container so it follows the new TryFinally and provides the return value. + Block leaveBlock = null; + if (afterFinallyExit is Branch brToLeave) + leaveBlock = brToLeave.TargetBlock; + + // Build the CFG once, before any structural changes. The dominator analysis below + // uses this snapshot to identify which blocks belong to the finally body. + var cfg = new ControlFlowGraph(container, context.CancellationToken); + + // Build a new BlockContainer for the finally body. + var finallyContainer = new BlockContainer().WithILRange(handler.Body); + + // Replace the TryCatch with a TryFinally now so that the move below places blocks into + // the freshly attached finallyContainer (which needs a parent to satisfy invariants). + BlockContainer tryBlockContainer = (BlockContainer)tryCatch.TryBlock; + var tryFinally = new TryFinally(tryBlockContainer, finallyContainer).WithILRange(tryCatch); + tryCatch.ReplaceWith(tryFinally); + + // Move the finally body — all blocks dominated by `continuation`, stopping at + // `finallyExitBlock` (so dispatchHead, captureBlock, throwBlock, and the leave block + // stay in the outer container). + AwaitInFinallyTransform.MoveDominatedBlocksToContainer(continuation, finallyExitBlock, + cfg, finallyContainer, context); + + // Strip the trailing dispatch-check (last 2 instructions: if + br dispatchHead) from + // the finally exit block, and end with `leave finallyContainer`. + RewriteFinallyExit(finallyExitBlock, finallyContainer); + + // Redirect try-block branches that targeted `continuation` to `Leave(tryBlockContainer)` + // so normal try-block completion runs the finally, then resumes after the TryFinally. + foreach (var br in tryBlockContainer.Descendants.OfType().ToArray()) + { + if (br.TargetBlock == continuation) + br.ReplaceWith(new Leave(tryBlockContainer).WithILRange(br)); + } + + // Append a successor instruction so the parent block remains EndPointUnreachable. + // If there was a separate leave-block, branch to it (it stays in the outer container); + // otherwise reuse the original Leave-with-value — RewriteFinallyExit already detached + // it from the if-instruction that previously held it. + if (leaveBlock != null) + parentBlock.Instructions.Add(new Branch(leaveBlock).WithILRange(afterFinallyExit)); + else + parentBlock.Instructions.Add(afterFinallyExit); + + // Remove the pre-init `stloc obj(ldnull)`. Also remove any dead + // `stloc (ldc.i4 0)` immediately preceding the TryFinally — the runtime-async + // lowering pre-allocates a flag local even for try-finally where it's never read, + // and leaving it between the resource store and the TryFinally would block + // UsingTransform from recognizing the using/await foreach pattern. + parentBlock.Instructions.RemoveAt(flagInitStore.ChildIndex); + RemoveDeadFlagStores(parentBlock, tryFinally); + + // Dispatch blocks are now unreachable; SortBlocks at the end of Run will drop them. + context.StepEndGroup(keepIfEmpty: true); + return true; + } + + // Scan instructions before `tryCatch` for the runtime-async flag-init store that matches + // `predicate`. The lowering inserts an `stloc obj(ldnull)` (try-finally) or + // `stloc num(ldc.i4 0)` (try-catch) before the try region; the catch handler overwrites it, + // the continuation reads it to decide whether an exception occurred (and which case). + // Returns null when no matching store is found. + static StLoc FindFlagInitStore(Block parentBlock, TryCatch tryCatch, Predicate predicate) + { + int tryIndex = tryCatch.ChildIndex; + for (int i = 0; i < tryIndex; i++) + { + if (parentBlock.Instructions[i] is StLoc s && predicate(s)) + return s; + } + return null; + } + + // Remove `stloc v(ldc.i4 0)` instructions immediately preceding `tryFinally` whose target + // variable is never read. + static void RemoveDeadFlagStores(Block parentBlock, TryFinally tryFinally) + { + while (tryFinally.ChildIndex > 0 + && parentBlock.Instructions[tryFinally.ChildIndex - 1] is StLoc deadStore + && deadStore.Value.MatchLdcI4(0) + && deadStore.Variable.LoadCount == 0) + { + parentBlock.Instructions.RemoveAt(deadStore.ChildIndex); + } + } + + // Locate the "if (obj == null) leave outer; br dispatchHead" finally-exit block, plus all dispatch blocks. + static bool FindFinallyDispatchExit(Block start, ILVariable objectVariable, BlockContainer container, + out Block finallyExitBlock, out List dispatchBlocks, out ILInstruction afterFinallyExit) + { + finallyExitBlock = null; + dispatchBlocks = null; + afterFinallyExit = null; + + // Walk reachable blocks until we find a block whose body matches the finally-exit shape. + var visited = new HashSet(); + var queue = new Queue(); + queue.Enqueue(start); + while (queue.Count > 0) + { + var b = queue.Dequeue(); + if (!visited.Add(b)) + continue; + if (MatchFinallyExitBlock(b, objectVariable, out var dispatchHead, out afterFinallyExit)) + { + finallyExitBlock = b; + if (CollectDispatchBlocks(dispatchHead, objectVariable, out dispatchBlocks)) + return true; + return false; + } + foreach (var br in b.Descendants.OfType()) + { + if (br.TargetBlock?.Parent == container) + queue.Enqueue(br.TargetBlock); + } + } + return false; + } + + // Match a tail of the form + // if (comp.o(ldloc obj == ldnull)) + // br dispatchHead + // (trailing two instructions of the block; instructions before are part of the finally body). + // The if-true branch may go directly to a function leave, to a one-instruction leave block, or to + // any other block in the outer container (e.g. an early-return flag check from a nested catch). + static bool MatchFinallyExitBlock(Block block, ILVariable objectVariable, out Block dispatchHead, out ILInstruction afterFinallyExit) + { + dispatchHead = null; + afterFinallyExit = null; + + if (block.Instructions.Count < 2) + return false; + if (block.Instructions[^2] is not IfInstruction ifInst) + return false; + if (!ifInst.Condition.MatchCompEqualsNull(out var arg) || !arg.MatchLdLoc(objectVariable)) + return false; + afterFinallyExit = ifInst.TrueInst; + if (afterFinallyExit is Leave leaveOuter && leaveOuter.IsLeavingFunction) + { + // direct leave OK + } + else if (afterFinallyExit is Branch brTarget) + { + // Branch to another block — could be a single-instruction leave block (canonical) or + // a non-trivial successor (e.g. an early-return check from a nested catch). + if (brTarget.TargetBlock.Instructions.Count == 1 + && brTarget.TargetBlock.Instructions[0] is Leave leaveOuter2 + && leaveOuter2.IsLeavingFunction) + { + afterFinallyExit = leaveOuter2; + } + // otherwise: keep the Branch as afterFinallyExit so the post-TryFinally successor is wired up + } + else + { + return false; + } + if (!block.Instructions[^1].MatchBranch(out dispatchHead)) + return false; + return true; + } + + // Block dispatchHead { + // stloc tmp(isinst Exception(ldloc obj)) + // if (comp.o(ldloc tmp != ldnull)) br captureBlock + // br throwBlock + // } + // Block captureBlock { callvirt Throw(call Capture(ldloc tmp)); leave outer } + // Block throwBlock { throw(ldloc obj) } + static bool CollectDispatchBlocks(Block dispatchHead, ILVariable objectVariable, out List dispatchBlocks) + { + dispatchBlocks = null; + if (dispatchHead.Instructions.Count != 3) + return false; + if (!dispatchHead.Instructions[0].MatchStLoc(out var typedExVar, out var typedExValue)) + return false; + if (!typedExValue.MatchIsInst(out var isInstArg, out _)) + return false; + if (!isInstArg.MatchLdLoc(objectVariable)) + return false; + if (dispatchHead.Instructions[1] is not IfInstruction ifInst) + return false; + if (!ifInst.Condition.MatchCompNotEqualsNull(out var notNullArg) || !notNullArg.MatchLdLoc(typedExVar)) + return false; + if (ifInst.TrueInst is not Branch toCapture) + return false; + if (dispatchHead.Instructions[2] is not Branch toThrow) + return false; + + dispatchBlocks = new List { dispatchHead, toCapture.TargetBlock, toThrow.TargetBlock }; + return true; + } + + static void RewriteFinallyExit(Block finallyExitBlock, BlockContainer finallyContainer) + { + // Strip the trailing 2 instructions (if + br dispatchHead) and append `leave finallyContainer`. + // The new Leave occupies the same end-of-finally position the removed dispatch check sat at, + // so inherit the IL range from both to keep source mapping aligned. + var ifInst = finallyExitBlock.Instructions[^2]; + var brInst = finallyExitBlock.Instructions[^1]; + finallyExitBlock.Instructions.RemoveRange(finallyExitBlock.Instructions.Count - 2, 2); + var leave = new Leave(finallyContainer).WithILRange(ifInst); + leave.AddILRange(brInst); + finallyExitBlock.Instructions.Add(leave); + } + + static List CollectReachable(Block entry, List exclude) + { + var excludeSet = new HashSet(exclude); + var visited = new HashSet(); + var result = new List(); + var queue = new Queue(); + queue.Enqueue(entry); + while (queue.Count > 0) + { + var b = queue.Dequeue(); + if (!visited.Add(b)) + continue; + if (excludeSet.Contains(b)) + continue; + if (b.Parent != entry.Parent) + continue; + result.Add(b); + foreach (var br in b.Descendants.OfType()) + { + if (br.TargetBlock != null && br.TargetBlock.Parent == entry.Parent) + queue.Enqueue(br.TargetBlock); + } + } + return result; + } + + // stloc num(0) + // .try { ... ; br continuation } + // catch ex : T when (ldc.i4 1) { + // [stloc tmp(ldloc ex);] + // [stloc obj(ldloc tmp_or_ex);] + // stloc num(1) + // br continuation + // } + // Block continuation { + // if (comp.i4(num != 1)) leave outer ; or "if (num == 1) br catchBody; leave outer" + // br catchBody + // } + // Block catchBody { ... } + // => + // .try { ... ; br continuation } + // catch ex : T { ...catchBody, with reads of obj as ex... } + // Block continuation { + // leave outer + // } + static bool TryRewriteTryCatch(TryCatch tryCatch, TryCatchHandler handler, Block handlerBlock, + Block parentBlock, BlockContainer container, + Dictionary filterObjByHandler, ILTransformContext context) + { + // Match catch body: [stloc tmp(ldloc ex);] [stloc obj(ldloc tmp);] stloc num(1); br continuation + if (handlerBlock.Instructions.Count < 2) + return false; + if (!handlerBlock.Instructions.Last().MatchBranch(out var continuation)) + return false; + if (continuation.Parent != container) + return false; + + ILVariable numVariable; + ILInstruction numStore = handlerBlock.Instructions[^2]; + if (!numStore.MatchStLoc(out numVariable, out var numValue)) + return false; + if (!numValue.MatchLdcI4(1)) + return false; + if (!numVariable.Type.IsKnownType(KnownTypeCode.Int32)) + return false; + + // Collect optional tmp/obj stores (everything before the num=1 store) + ILVariable objectVariable = null; + ILVariable temporaryVariable = null; + int prefixCount = handlerBlock.Instructions.Count - 2; + if (prefixCount >= 1) + { + // Last prefix instruction may be: stloc obj(ldloc tmp_or_ex) + if (handlerBlock.Instructions[prefixCount - 1].MatchStLoc(out var v, out var val)) + { + if (val.MatchLdLoc(handler.Variable)) + { + // Direct stloc obj(ldloc ex) — no tmp variable + objectVariable = v; + prefixCount--; + } + else if (val.MatchLdLoc(out var tmpV) && prefixCount >= 2 + && handlerBlock.Instructions[prefixCount - 2].MatchStLoc(out var tmpV2, out var tmpVal) + && tmpV == tmpV2 && tmpVal.MatchLdLoc(handler.Variable)) + { + temporaryVariable = tmpV; + objectVariable = v; + prefixCount -= 2; + } + else + { + return false; + } + } + } + if (prefixCount != 0) + return false; + + // Pre-try: somewhere before the TryCatch we expect `stloc num(ldc.i4 0)`. + var flagInitStore = FindFlagInitStore(parentBlock, tryCatch, + s => s.Variable == numVariable && s.Value.MatchLdcI4(0)); + if (flagInitStore == null) + return false; + + // Continuation must contain a "num == 1" check that branches to the catch body, or + // alternatively a "num != 1" check that leaves outer. + if (!MatchCatchEntryCheck(continuation, numVariable, container, + out var catchBodyEntry, out var afterCatchExit)) + return false; + + context.StepStartGroup("Rewrite runtime-async try-catch", tryCatch); + + // Move catch body blocks (those dominated by catchBodyEntry within `container`) into the handler. + var bodyBlocks = CollectReachable(catchBodyEntry, new List()); + foreach (var b in bodyBlocks) + { + b.Remove(); + } + + // Replace handler's existing block (which was just the prefix + num=1 + branch) + // with the catch body. Preserve the original branch target redirection: branches that + // targeted `continuation` from inside the body now target the new continuation + // (the `leave outer` block which is `continuation` itself, after we strip its catch-entry-check). + handlerBlock.Instructions.Clear(); + handlerBlock.Instructions.Add(new Branch(catchBodyEntry)); + foreach (var b in bodyBlocks) + ((BlockContainer)handler.Body).Blocks.Add(b); + + // Replace reads of `obj` (and `tmp`) inside the moved catch body with reads of handler.Variable. + // `objectVariable` here is the handler's body-level obj (only present for non-filter catches); + // `filterObj` is the obj recorded by NormalizeRuntimeAsyncFilter when the catch carries a filter. + if (objectVariable != null) + ReplaceVariableReadsWithHandlerVariable(handler.Body, objectVariable, handler.Variable); + if (temporaryVariable != null) + ReplaceVariableReadsWithHandlerVariable(handler.Body, temporaryVariable, handler.Variable); + if (filterObjByHandler.TryGetValue(handler, out var filterObj)) + ReplaceVariableReadsWithHandlerVariable(handler.Body, filterObj, handler.Variable); + + // Inside the moved blocks, locate any leftover dispatch idiom (originating from `throw;`) + // and replace it with `Rethrow`. + foreach (var b in handler.Body.Descendants.OfType().ToArray()) + ReplaceDispatchIdiomWithRethrow(b, handler.Variable, context); + + // Strip the catch-entry check from `continuation` — replace with a raw `leave outer`. + // Clear() already detached `afterCatchExit` (it was either continuation.Instructions[1] + // or a child of the now-detached if-instruction), so we can re-add it directly. + continuation.Instructions.Clear(); + continuation.Instructions.Add(afterCatchExit); + + // Remove the pre-try `stloc num(0)`. + parentBlock.Instructions.RemoveAt(flagInitStore.ChildIndex); + + context.StepEndGroup(keepIfEmpty: true); + return true; + } + + // Multi-handler runtime-async try-catch: + // + // stloc num(0) + // .try { ... ; br continuation } + // catch ex_1 : T_1 when (...) { [stloc tmp1(ex_1);] [stloc obj(...);] stloc num(K_1); br continuation } + // catch ex_2 : T_2 when (...) { [stloc tmp2(ex_2);] [stloc obj(...);] stloc num(K_2); br continuation } + // ... + // Block continuation { + // switch (ldloc num) { case [K_i..K_i+1): br case_K_i ... ; default: leave outer } + // } + // Block case_K_i { } + // + // => + // .try { ... ; br continuation } + // catch ex_1 : T_1 when (...) { ...case body 1 inlined, with obj/tmp reads remapped to ex_1... } + // catch ex_2 : T_2 when (...) { ...case body 2 inlined, with obj/tmp reads remapped to ex_2... } + // ... + // Block continuation { + // leave outer + // } + static bool TryRewriteMultiHandlerTryCatch(TryCatch tryCatch, Block parentBlock, BlockContainer container, + Dictionary filterObjByHandler, ILTransformContext context) + { + Block continuation = null; + ILVariable numVariable = null; + var seenK = new HashSet(); + var handlerInfos = new List<(TryCatchHandler handler, int k, ILVariable bodyObj, ILVariable bodyTmp)>(); + + foreach (var handler in tryCatch.Handlers) + { + if (handler.Body is not BlockContainer hb || hb.Blocks.Count != 1) + return false; + var hblock = hb.Blocks[0]; + if (hblock.Instructions.Count < 2) + return false; + if (!hblock.Instructions.Last().MatchBranch(out var br)) + return false; + if (continuation == null) + continuation = br; + else if (continuation != br) + return false; + if (continuation.Parent != container) + return false; + + if (!hblock.Instructions[^2].MatchStLoc(out var nv, out var nval)) + return false; + if (!nval.MatchLdcI4(out int k)) + return false; + if (!nv.Type.IsKnownType(KnownTypeCode.Int32)) + return false; + if (numVariable == null) + numVariable = nv; + else if (numVariable != nv) + return false; + if (!seenK.Add(k)) + return false; + + ILVariable bodyObj = null, bodyTmp = null; + int prefix = hblock.Instructions.Count - 2; + if (prefix >= 1 && hblock.Instructions[prefix - 1].MatchStLoc(out var v, out var val)) + { + if (val.MatchLdLoc(handler.Variable)) + { + bodyObj = v; + prefix--; + } + else if (val.MatchLdLoc(out var tmpV) && prefix >= 2 + && hblock.Instructions[prefix - 2].MatchStLoc(out var tmpV2, out var tmpVal) + && tmpV == tmpV2 && tmpVal.MatchLdLoc(handler.Variable)) + { + bodyTmp = tmpV; + bodyObj = v; + prefix -= 2; + } + else + { + return false; + } + } + if (prefix != 0) + return false; + + handlerInfos.Add((handler, k, bodyObj, bodyTmp)); + } + + // Pre-try: stloc num(ldc.i4 0). After SplitVariables the pre-init's local may be a + // separate ILVariable (the in-handler stores form a disjoint use that gets split off), + // so match the slot and type rather than the ILVariable identity. + var flagInitStore = FindFlagInitStore(parentBlock, tryCatch, + s => s.Variable.Index == numVariable.Index + && s.Variable.Kind == numVariable.Kind + && s.Variable.Type.IsKnownType(KnownTypeCode.Int32) + && s.Value.MatchLdcI4(0)); + if (flagInitStore == null) + return false; + // Block continuation { switch (ldloc num) { case K: br case_K ... ; default: leave outer } } + if (!MatchSwitchDispatch(continuation, numVariable, out var caseBlocks, out var defaultExit)) + return false; + // Every K we recorded must have a case in the switch. + foreach (var info in handlerInfos) + if (!caseBlocks.ContainsKey(info.k)) + return false; + + context.StepStartGroup("Rewrite runtime-async multi-handler try-catch", tryCatch); + var cfg = new ControlFlowGraph(container, context.CancellationToken); + + foreach (var info in handlerInfos) + { + var caseBody = caseBlocks[info.k]; + var handler = info.handler; + var handlerBody = (BlockContainer)handler.Body; + var handlerEntry = handlerBody.Blocks[0]; + + // Move case-body and dominated blocks into the handler's body container. + AwaitInFinallyTransform.MoveDominatedBlocksToContainer(caseBody, null, cfg, handlerBody, context); + + // Replace the handler-entry block (still holds the prefix + num=K + branch) with a + // single `br caseBody`, since caseBody is now at index >= 1 in handlerBody. + handlerEntry.Instructions.Clear(); + handlerEntry.Instructions.Add(new Branch(caseBody)); + + // Remap the per-handler synthesized variables to the handler.Variable inside the moved body. + if (info.bodyObj != null) + ReplaceVariableReadsWithHandlerVariable(handler.Body, info.bodyObj, handler.Variable); + if (info.bodyTmp != null) + ReplaceVariableReadsWithHandlerVariable(handler.Body, info.bodyTmp, handler.Variable); + if (filterObjByHandler.TryGetValue(handler, out var filterObj)) + ReplaceVariableReadsWithHandlerVariable(handler.Body, filterObj, handler.Variable); + + foreach (var b in handler.Body.Descendants.OfType().ToArray()) + ReplaceDispatchIdiomWithRethrow(b, handler.Variable, context); + } + + // Replace continuation with the default exit (leave outer). + // Clear() already detached defaultExit via the SwitchInstruction's release-ref cascade, + // so we can re-add it directly. + continuation.Instructions.Clear(); + continuation.Instructions.Add(defaultExit); + + // Remove the pre-try `stloc num(0)`. + parentBlock.Instructions.RemoveAt(flagInitStore.ChildIndex); + + context.StepEndGroup(keepIfEmpty: true); + return true; + } + + // Block continuation { switch (ldloc num) { case [K..K+1): br case_K ... ; default: } } + static bool MatchSwitchDispatch(Block continuation, ILVariable numVariable, + out Dictionary caseBlocks, out ILInstruction defaultExit) + { + caseBlocks = null; + defaultExit = null; + if (continuation.Instructions.Count != 1) + return false; + if (continuation.Instructions[0] is not SwitchInstruction switchInst) + return false; + if (!switchInst.Value.MatchLdLoc(numVariable)) + return false; + + var defaultSection = switchInst.GetDefaultSection(); + if (defaultSection == null) + return false; + if (defaultSection.Body is Leave defLeave && defLeave.IsLeavingFunction) + defaultExit = defLeave; + else if (defaultSection.Body is Branch) + defaultExit = defaultSection.Body; + else + return false; + + caseBlocks = new Dictionary(); + foreach (var section in switchInst.Sections) + { + if (section == defaultSection) + continue; + if (!section.Body.MatchBranch(out var caseBlock)) + return false; + // Each non-default section's labels must cover exactly one integer value (K). + if (section.Labels.Count() != 1) + return false; + caseBlocks[(int)section.Labels.Intervals[0].Start] = caseBlock; + } + return true; + } + + // Block continuation { + // Variant A: if (comp.i4(num != 1)) leave outer; br catchBody + // Variant B: if (comp.i4(num == 1)) br catchBody; leave outer + // } + static bool MatchCatchEntryCheck(Block continuation, ILVariable numVariable, BlockContainer container, + out Block catchBodyEntry, out ILInstruction afterCatchExit) + { + catchBodyEntry = null; + afterCatchExit = null; + + if (continuation.Instructions.Count != 2) + return false; + if (continuation.Instructions[0] is not IfInstruction ifInst) + return false; + + // Equals form: if (num == 1) br catchBody ; + if (ifInst.Condition.MatchCompEquals(out var lhs, out var rhs) + && lhs.MatchLdLoc(numVariable) && rhs.MatchLdcI4(1) + && ifInst.TrueInst is Branch eqBranch + && continuation.Instructions[1] is Leave eqLeave && eqLeave.IsLeavingFunction) + { + catchBodyEntry = eqBranch.TargetBlock; + afterCatchExit = eqLeave; + return catchBodyEntry?.Parent == container; + } + + // Not-equals form: if (num != 1) leave outer ; br catchBody + if (ifInst.Condition.MatchCompNotEquals(out lhs, out rhs) + && lhs.MatchLdLoc(numVariable) && rhs.MatchLdcI4(1) + && ifInst.TrueInst is Leave neLeave && neLeave.IsLeavingFunction + && continuation.Instructions[1] is Branch neBranch) + { + catchBodyEntry = neBranch.TargetBlock; + afterCatchExit = neLeave; + return catchBodyEntry?.Parent == container; + } + + return false; + } + + static void ReplaceVariableReadsWithHandlerVariable(ILInstruction root, ILVariable from, ILVariable to) + { + foreach (var ldloc in root.Descendants.OfType().ToArray()) + { + if (ldloc.Variable != from) + continue; + // If parent is a CastClass to handler.Variable.Type or a base, inline directly. + if (ldloc.Parent is CastClass cc && cc.Type.Equals(to.Type)) + { + cc.ReplaceWith(new LdLoc(to).WithILRange(cc).WithILRange(ldloc)); + } + else + { + ldloc.ReplaceWith(new LdLoc(to).WithILRange(ldloc)); + } + } + foreach (var stloc in root.Descendants.OfType().ToArray()) + { + if (stloc.Variable == from) + { + // Drop dead stores to the synthesized variable. + if (stloc.Parent is Block parentBlock) + { + parentBlock.Instructions.RemoveAt(stloc.ChildIndex); + } + } + } + } + + // Recognize the runtime-async lowering of an early return that crosses a try-finally. + // Roslyn rewrites `return value;` inside a try-block as: + // stloc capture(value) + // stloc flag(K) + // leave-try (i.e. let the finally run, then exit the try-finally) + // followed by post-try logic of the form: + // if (flag == K) leave outer (capture) + // + // Detect that pattern around a TryFinally we just produced and rewrite each capture-set-flag-and-leave + // site into a direct `leave outer (value)`, then drop the flag/post-flag-check machinery. The leave + // still passes through the TryFinally so the user's finally body runs before the function returns, + // which is the intended source-level semantics of `return` from inside a try-finally. + static bool TryRewriteFlagBasedEarlyReturn(TryFinally tryFinally, ILTransformContext context) + { + if (tryFinally.Parent is not Block parentBlock) + return false; + if (parentBlock.Parent is not BlockContainer container) + return false; + + // The TryFinally is followed in parentBlock by either nothing (fall-through into the next block) + // or a single `br checkBlock` we appended ourselves. Locate the post-try check block. + Block checkBlock; + int tryFinallyIdx = tryFinally.ChildIndex; + if (tryFinallyIdx == parentBlock.Instructions.Count - 1) + return false; + if (parentBlock.Instructions[tryFinallyIdx + 1] is Branch br) + checkBlock = br.TargetBlock; + else + return false; + if (checkBlock?.Parent != container) + return false; + + // Block checkBlock { if (flagVar == K) br earlyBlock; br normalBlock } + // or: { if (flagVar != K) br normalBlock; br earlyBlock } + if (checkBlock.Instructions.Count != 2) + return false; + if (checkBlock.Instructions[0] is not IfInstruction ifInst) + return false; + if (ifInst.TrueInst is not Branch toIfTrue) + return false; + if (!checkBlock.Instructions[1].MatchBranch(out var fallthroughBlock)) + return false; + + ILVariable flagVar; + int targetK; + Block earlyBlock, normalBlock; + if (ifInst.Condition.MatchCompEquals(out var lhs, out var rhs) + && lhs.MatchLdLoc(out flagVar) && rhs.MatchLdcI4(out targetK)) + { + earlyBlock = toIfTrue.TargetBlock; + normalBlock = fallthroughBlock; + } + else if (ifInst.Condition.MatchCompNotEquals(out lhs, out rhs) + && lhs.MatchLdLoc(out flagVar) && rhs.MatchLdcI4(out targetK)) + { + normalBlock = toIfTrue.TargetBlock; + earlyBlock = fallthroughBlock; + } + else + { + return false; + } + if (!flagVar.Type.IsKnownType(KnownTypeCode.Int32)) + return false; + if (earlyBlock?.Parent != container || normalBlock?.Parent != container) + return false; + + // earlyBlock chain: optional `stloc returnVar(ldloc capture); br leaveBlock` followed by + // `leave outer (ldloc returnVar)` (or a direct leave with the capture). + if (!ResolveEarlyReturnValue(earlyBlock, container, out var captureVar, out var returnVar, out var leaveBlock)) + return false; + + // Inside the try-block find the flag-setter block(s): `stloc flagVar(K); leave-tryBlock`. + // There may be multiple — e.g. several catches in a multi-handler try each with its own + // early-return — but for the simple case we only need one. + if (tryFinally.TryBlock is not BlockContainer tryBlockContainer) + return false; + var flagSetters = new List(); + foreach (var b in tryBlockContainer.Blocks) + { + if (b.Instructions.Count == 2 + && b.Instructions[0] is StLoc setStore + && setStore.Variable == flagVar + && setStore.Value.MatchLdcI4(targetK) + && b.Instructions[1] is Leave leaveFromTry + && leaveFromTry.TargetContainer == tryBlockContainer) + { + flagSetters.Add(b); + } + } + if (flagSetters.Count == 0) + return false; + + // Verify flagVar is only set in flag-setters and the pre-try init (`stloc flagVar(0)`). + foreach (var store in flagVar.StoreInstructions.OfType()) + { + if (flagSetters.Any(fs => fs.Instructions.Contains(store))) + continue; + if (store.Parent == parentBlock && store.Value.MatchLdcI4(0)) + continue; + return false; + } + + // Build the leave instruction shape: leave outer (ldloc captureSource). + var outerContainer = (BlockContainer)leaveBlock.Parent; + var outerLeave = (Leave)leaveBlock.Instructions[^1]; + if (outerLeave.TargetContainer != outerContainer) + return false; + + context.StepStartGroup("Reduce runtime-async flag-based early return", tryFinally); + + // Single pass over the try body: every Branch targeting a flag-setter is the tail of an + // early-return site (`...; stloc capture(value); br fs`). Replace each with a direct + // `leave outer (ldloc capture)`. The capture store stays and feeds the leave value via the + // variable read. Then drop the flag-setter blocks; they're unreachable post-rewrite. + var flagSetterSet = new HashSet(flagSetters); + foreach (var pred in tryBlockContainer.Descendants.OfType().ToArray()) + { + if (!flagSetterSet.Contains(pred.TargetBlock)) + continue; + pred.ReplaceWith(new Leave(outerContainer, new LdLoc(captureVar)).WithILRange(pred)); + } + foreach (var fs in flagSetters) + fs.Remove(); + + // The post-flag-check block can now be replaced with `br normalBlock`. The flag write/read, + // the early-return chain and (eventually) the captureVar/returnVar become unreferenced. + checkBlock.Instructions.Clear(); + checkBlock.Instructions.Add(new Branch(normalBlock)); + + // Drop the pre-try `stloc flagVar(0)`. + for (int i = 0; i < tryFinallyIdx; i++) + { + if (parentBlock.Instructions[i] is StLoc s && s.Variable == flagVar && s.Value.MatchLdcI4(0)) + { + parentBlock.Instructions.RemoveAt(i); + tryFinallyIdx--; + i--; + } + } + + context.StepEndGroup(keepIfEmpty: true); + return true; + } + + // earlyBlock should be either: + // `stloc returnVar(ldloc capture); br leaveBlock` followed by leaveBlock = `leave outer (ldloc returnVar)` + // or a direct `leave outer (ldloc capture)`. + static bool ResolveEarlyReturnValue(Block earlyBlock, BlockContainer container, + out ILVariable captureVar, out ILVariable returnVar, out Block leaveBlock) + { + captureVar = null; + returnVar = null; + leaveBlock = null; + + // Direct shape: earlyBlock is just `leave outer (ldloc capture)`. + if (earlyBlock.Instructions.Count == 1 + && earlyBlock.Instructions[0] is Leave directLeave + && directLeave.IsLeavingFunction + && directLeave.Value.MatchLdLoc(out captureVar)) + { + leaveBlock = earlyBlock; + return true; + } + + // Indirected shape: earlyBlock copies the capture into a returnVar and branches to a + // one-instruction `leave outer (ldloc returnVar)` block. + if (earlyBlock.Instructions.Count != 2) + return false; + if (!earlyBlock.Instructions[0].MatchStLoc(out returnVar, out var rvValue)) + return false; + if (!rvValue.MatchLdLoc(out captureVar)) + return false; + if (!earlyBlock.Instructions[1].MatchBranch(out leaveBlock)) + return false; + if (leaveBlock.Parent != container) + return false; + if (leaveBlock.Instructions.Count != 1) + return false; + if (leaveBlock.Instructions[0] is not Leave finalLeave) + return false; + if (!finalLeave.IsLeavingFunction) + return false; + if (!finalLeave.Value.MatchLdLoc(returnVar)) + return false; + return true; + } + + static void ReplaceDispatchIdiomWithRethrow(Block block, ILVariable handlerVariable, ILTransformContext context) + { + // Reuse AwaitInCatchTransform.MatchExceptionCaptureBlock through the block-tail shape: + // stloc typedExVar(isinst Exception(ldloc handlerVariable)) + // if (comp.o(ldloc typedExVar != ldnull)) br captureBlock + // br throwBlock + // Block captureBlock { callvirt Throw(call Capture(ldloc typedExVar)); leave/br } + // Block throwBlock { throw(ldloc handlerVariable) } + ILVariable v = handlerVariable; + if (AwaitInCatchTransform.MatchExceptionCaptureBlock(context, block, ref v, + out var typedExceptionVariableStore, out var captureBlock, out var throwBlock)) + { + if (v != handlerVariable) + return; + // The Rethrow stands in for the whole dispatch idiom (this block's tail + the + // capture/throw blocks). Capture IL ranges from each component before the + // removals detach them, so source mapping stays anchored to the original bytes. + var rethrow = new Rethrow().WithILRange(typedExceptionVariableStore); + rethrow.AddILRange(block.Instructions[typedExceptionVariableStore.ChildIndex + 1]); + rethrow.AddILRange(block.Instructions[typedExceptionVariableStore.ChildIndex + 2]); + foreach (var inst in captureBlock.Instructions) + rethrow.AddILRange(inst); + foreach (var inst in throwBlock.Instructions) + rethrow.AddILRange(inst); + block.Instructions.RemoveRange(typedExceptionVariableStore.ChildIndex + 1, 2); + captureBlock.Remove(); + throwBlock.Remove(); + typedExceptionVariableStore.ReplaceWith(rethrow); + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs new file mode 100644 index 0000000000..61bce98877 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncManualAwaitTransform.cs @@ -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 +{ + /// + /// 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. + /// + static class RuntimeAsyncManualAwaitTransform + { + public static void Run(ILFunction function, ILTransformContext context) + { + bool changed = false; + foreach (var block in function.Descendants.OfType().ToArray()) + { + if (DetectRuntimeAsyncManualAwait(block, context)) + changed = true; + } + if (changed) + { + foreach (var c in function.Body.Descendants.OfType()) + c.SortBlocks(deleteUnreachableBlocks: true); + } + } + + // Block X (head) { + // [stloc awaitable();] (optional: when GetAwaiter takes an address) + // stloc awaiter(call GetAwaiter()) + // if (call get_IsCompleted(ldloca awaiter)) br completedBlock + // br pauseBlock + // } + // Block pauseBlock { + // call AsyncHelpers.UnsafeAwaitAwaiter[](ldloc awaiter) + // br completedBlock + // } + // Block completedBlock { + // call GetResult(ldloca awaiter) (possibly inlined into a containing expression) + // ... + // } + // => + // Block X { ...; br completedBlock } + // Block completedBlock { ; ... } + 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; + } + } +} diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 072943ebd1..dde9ded979 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -159,6 +159,7 @@ public ILReader(MetadataModule module) BitSet isBranchTarget = null!; BlockContainer mainContainer = null!; int currentInstructionStart; + bool isRuntimeAsync; Dictionary blocksByOffset = new Dictionary(); Queue importQueue = new Queue(); @@ -172,6 +173,8 @@ void Init(MethodDefinitionHandle methodDefinitionHandle, MethodBodyBlock body, G 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 @@ void Init(MethodDefinitionHandle methodDefinitionHandle, MethodBodyBlock body, G this.reader = body.GetILReader(); this.currentStack = ImmutableStack.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 @@ public ILFunction ReadIL(MethodDefinitionHandle method, MethodBodyBlock body, Ge 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; diff --git a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs index 431bd14e1e..bd315f05e3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/EarlyExpressionTransforms.cs @@ -173,6 +173,36 @@ internal static void AddressOfLdLocToLdLoca(LdObj inst, ILTransformContext conte } } + 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. + internal static bool TransformAsyncHelpersAwaitToAwait(Call inst, ILTransformContext context) + { + if (!context.Settings.RuntimeAsync) + 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)) diff --git a/ICSharpCode.Decompiler/SRMHacks.cs b/ICSharpCode.Decompiler/SRMHacks.cs index cf35ade43f..758860dd45 100644 --- a/ICSharpCode.Decompiler/SRMHacks.cs +++ b/ICSharpCode.Decompiler/SRMHacks.cs @@ -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 GetMethodImplementations( this MethodDefinitionHandle handle, MetadataReader reader) diff --git a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs index 79dc9b62a0..837628787e 100644 --- a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs +++ b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs @@ -154,12 +154,17 @@ public enum TypeSystemOptions /// ExtensionMembers = 0x80000, /// + /// If this option is active, methods with the MethodImplAttribute(MethodImplOptions.Async) are treated as async methods. + /// + RuntimeAsync = 0x100000, + /// /// Default settings: typical options for the decompiler, with all C# language features enabled. /// Default = Dynamic | Tuple | ExtensionMethods | DecimalConstants | ReadOnlyStructsAndParameters | RefStructs | UnmanagedConstraints | NullabilityAnnotations | ReadOnlyMethods | NativeIntegers | FunctionPointers | ScopedRef | NativeIntegersWithoutAttribute | RefReadOnlyParameters | ParamsCollections | FirstClassSpanTypes | ExtensionMembers + | RuntimeAsync } /// @@ -207,6 +212,8 @@ public static TypeSystemOptions GetOptions(DecompilerSettings settings) typeSystemOptions |= TypeSystemOptions.FirstClassSpanTypes; if (settings.ExtensionMembers) typeSystemOptions |= TypeSystemOptions.ExtensionMembers; + if (settings.RuntimeAsync) + typeSystemOptions |= TypeSystemOptions.RuntimeAsync; return typeSystemOptions; } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs index 7173d1850d..1f69799d07 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs @@ -350,6 +350,10 @@ public IEnumerable GetAttributes() 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 diff --git a/ICSharpCode.Decompiler/TypeSystem/TaskType.cs b/ICSharpCode.Decompiler/TypeSystem/TaskType.cs index f124d53ec5..4a6263ce5a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TaskType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TaskType.cs @@ -40,6 +40,28 @@ public static IType UnpackTask(ICompilation compilation, IType type) return type.TypeArguments[0]; } + /// + /// Gets the element type of any task-like type (Task, Task<T>, ValueTask, + /// ValueTask<T>, or any [AsyncMethodBuilder]-attributed custom task type). + /// Returns void for non-generic task-likes. Any other type is returned unmodified. + /// + 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; + } + /// /// Gets whether the specified type is Task or Task<T>. /// diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 939c927da9..7070b9df81 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -24,8 +24,6 @@ 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 @@ public override IReadOnlyList LanguageVersions { 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;