From 82edef3bc6f73d22995a88756bb48cddd6e28b3c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 15 Apr 2026 18:06:45 +0200 Subject: [PATCH] Add scaffolding for the C# 15 runtime-async lowering. Detect MethodImplOptions.Async (0x2000) in ILReader and unpack Task/Task return types so the IL Leave value and function.AsyncReturnType match the source signature. Add CSharp15_0 (Preview also bumped to 1500) and a RuntimeAsync setting (default on, gated to >=CSharp15_0), expose it in the Languages dropdown, mask the synthetic MethodImplAsync bit out of the decompiled [MethodImpl], and add a .runtimeasync test suffix. --- .../Helpers/Tester.cs | 2 ++ .../CSharp/CSharpLanguageVersion.cs | 3 +- ICSharpCode.Decompiler/DecompilerSettings.cs | 28 +++++++++++++++++-- ICSharpCode.Decompiler/IL/ILReader.cs | 19 ++++++++++++- ICSharpCode.Decompiler/SRMHacks.cs | 1 + .../TypeSystem/DecompilerTypeSystem.cs | 7 +++++ .../Implementation/MetadataMethod.cs | 4 +++ ILSpy/Languages/CSharpLanguage.cs | 3 +- ILSpy/Properties/Resources.Designer.cs | 11 +++++++- ILSpy/Properties/Resources.resx | 3 ++ 10 files changed, 74 insertions(+), 7 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index ffa2b9a40..22714e85a 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -848,6 +848,8 @@ namespace System.Runtime.CompilerServices suffix += ".mcs2"; if ((cscOptions & CompilerOptions.UseMcs5_23) != 0) suffix += ".mcs5"; + if ((cscOptions & CompilerOptions.EnableRuntimeAsync) != 0) + suffix += ".runtimeasync"; return suffix; } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs index 07e76b0c9..66c4cdb57 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs @@ -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 } } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 8853717ab..e9aeefb73 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -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 /// /// 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 @@ namespace ICSharpCode.Decompiler /// /// 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 @@ namespace ICSharpCode.Decompiler } } + 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/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 072943ebd..3faeeee96 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -159,6 +159,7 @@ namespace ICSharpCode.Decompiler.IL BitSet isBranchTarget = null!; BlockContainer mainContainer = null!; int currentInstructionStart; + bool isRuntimeAsync; Dictionary blocksByOffset = new Dictionary(); Queue importQueue = new Queue(); @@ -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 this.reader = body.GetILReader(); this.currentStack = ImmutableStack.Empty; this.expressionStack.Clear(); - this.methodReturnStackType = method.ReturnType.GetStackType(); + if (isRuntimeAsync) + { + this.methodReturnStackType = TaskType.UnpackTask(compilation, method.ReturnType).GetStackType(); + } + else + { + this.methodReturnStackType = method.ReturnType.GetStackType(); + } InitParameterVariables(); localVariables = InitLocalVariables(); foreach (var v in localVariables) @@ -715,6 +725,13 @@ 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) + { + if (!this.method.ReturnType.IsKnownType(KnownTypeCode.TaskOfT)) + function.AsyncReturnType = compilation.FindType(KnownTypeCode.Void); + else + function.AsyncReturnType = TaskType.UnpackTask(compilation, this.method.ReturnType); + } function.Variables.AddRange(parameterVariables); function.Variables.AddRange(localVariables); function.LocalVariableSignatureLength = localVariables.Length; diff --git a/ICSharpCode.Decompiler/SRMHacks.cs b/ICSharpCode.Decompiler/SRMHacks.cs index cf35ade43..758860dd4 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 79dc9b62a..837628787 100644 --- a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs +++ b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs @@ -154,12 +154,17 @@ namespace ICSharpCode.Decompiler.TypeSystem /// 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 @@ namespace ICSharpCode.Decompiler.TypeSystem 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 7173d1850..1f69799d0 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs @@ -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 diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 939c927da..7070b9df8 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -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 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; diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index b96523d85..ba7df50e3 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1360,7 +1360,16 @@ namespace ICSharpCode.ILSpy.Properties { return ResourceManager.GetString("DecompilerSettings.RequiredMembers", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Decompile runtime-async (System.Runtime.CompilerServices.AsyncHelpers) methods. + /// + public static string DecompilerSettings_RuntimeAsync { + get { + return ResourceManager.GetString("DecompilerSettings.RuntimeAsync", resourceCulture); + } + } + /// /// Looks up a localized string similar to 'scoped' lifetime annotation. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index aa5fff5fd..5accafe41 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -474,6 +474,9 @@ Are you sure you want to continue? Required members + + Decompile runtime-async (System.Runtime.CompilerServices.AsyncHelpers) methods + 'scoped' lifetime annotation