Repository: dotnet/roslyn
Pull request #77543: Add RuntimeAsyncMethodGenerationAttribute
Author: login: 333fred
Labels: name: Area-Compilers
name: untriaged
description: Issues and PRs which have not yet been triaged by a lead
name: Feature - Runtime Async
Created: 2025-03-11T22:02:57Z | Merged: 2025-03-19T19:42:51Z

Adds control for whether to use runtime async. The flowchart is as follows:

1. The flag `System.Runtime.CompilerServices.RuntimeFeature.Async` must be present.
2. Assuming that flag is present, we look for the presence of `System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute` on the method. If that attribute is present, we use the preference expressed in the attribute. The preference does not carry to nested contexts, such as local functions or lambdas.
3. If the attribute is not present, we look for `features:runtime-async=on` on the command line. If that is present, then the feature is on by default. Otherwise, the feature is off.

Relates to test plan https://github.com/dotnet/roslyn/issues/75960


Diff:
diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
index e04d7b0dccb28..4c141a2d75f5a 100644
--- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
+++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
@@ -316,9 +316,7 @@ internal bool IsNullableAnalysisEnabledAlways
         /// Returns true if this method should be processed with runtime async handling instead
         /// of compiler async state machine generation.
         /// </summary>
-#pragma warning disable IDE0060 // Remove unused parameter
         internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
-#pragma warning restore IDE0060 // Remove unused parameter
         {
             // PROTOTYPE: EE tests fail this assert, handle and test
             //Debug.Assert(ReferenceEquals(method.ContainingAssembly, Assembly));
@@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
+            };
         }
 
         /// <summary>
diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs
index e95f295b21e87..c9410bc2f831c 100644
--- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs
+++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs
@@ -149,5 +149,21 @@ public UnmanagedCallersOnlyAttributeData? UnmanagedCallersOnlyAttributeData
                 SetDataStored();
             }
         }
+
+        private ThreeState _runtimeAsyncMethodGenerationSetting;
+        public ThreeState RuntimeAsyncMethodGenerationSetting
+        {
+            get
+            {
+                VerifySealed(expected: true);
+                return _runtimeAsyncMethodGenerationSetting;
+            }
+            set
+            {
+                VerifySealed(expected: false);
+                _runtimeAsyncMethodGenerationSetting = value;
+                SetDataStored();
+            }
+        }
     }
 }
diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs
index 9a50eea9844f5..dbd6e7f2a2f6a 100644
--- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs
+++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs
@@ -651,6 +651,14 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut
                         arguments.AttributeSyntaxOpt);
                 }
             }
+            else if (attribute.IsTargetAttribute(AttributeDescription.RuntimeAsyncMethodGenerationAttribute))
+            {
+                // PROTOTYPE: Validate langversion? Validate previewness of the runtime feature flag?
+                arguments.GetOrCreateData<MethodWellKnownAttributeData>().RuntimeAsyncMethodGenerationSetting =
+                    attribute.CommonConstructorArguments[0].DecodeValue<bool>(SpecialType.System_Boolean)
+                        ? ThreeState.True
+                        : ThreeState.False;
+            }
             else
             {
                 var compilation = this.DeclaringCompilation;
@@ -661,6 +669,9 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut
             }
         }
 
+        internal ThreeState IsRuntimeAsyncEnabledInMethod
+            => GetDecodedWellKnownAttributeData()?.RuntimeAsyncMethodGenerationSetting ?? ThreeState.Unknown;
+
         internal override ImmutableArray<string> NotNullMembers =>
             GetDecodedWellKnownAttributeData()?.NotNullMembers ?? ImmutableArray<string>.Empty;
 
diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
index 66126dc35c666..2807e211f1817 100644
--- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
+++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
@@ -25,6 +25,7 @@ public class CodeGenAsyncTests : EmitMetadataTestBase
     {
         // PROTOTYPE: Use the real value when possible
         private const MethodImplAttributes MethodImplOptionsAsync = (MethodImplAttributes)1024;
+        private static CSharpParseOptions WithRuntimeAsync(CSharpParseOptions options) => options.WithFeature("runtime-async", "on");
 
         internal static string ExpectedOutput(string output)
         {
@@ -173,7 +174,7 @@ public static void Main()
 ";
             CompileAndVerify(source, expectedOutput: expected);
 
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var verifier = CompileAndVerify(comp, verify: Verification.Fails with
@@ -240,7 +241,7 @@ public static async Task Main()
 }";
 
             //var expected = "42";
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var verifier = CompileAndVerify(comp, verify: Verification.Fails with
@@ -302,7 +303,7 @@ public static void Main()
 ";
             CompileAndVerify(source, expectedOutput: expected);
 
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var verifier = CompileAndVerify(comp, verify: Verification.Fails with
@@ -363,7 +364,7 @@ public static async Task Main()
     }
 }";
             //var expected = @"O brave new world...";
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var verifier = CompileAndVerify(comp, verify: Verification.Fails with
@@ -421,7 +422,7 @@ public static async Task Main()
                     }
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -561,7 +562,7 @@ public static async Task Main()
                     }
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -686,7 +687,7 @@ public static async Task Main()
                     }
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -800,7 +801,7 @@ public static async Task Main()
                     }
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -932,7 +933,7 @@ public static async Task Main()
                     }
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -1046,7 +1047,7 @@ public static async Task Main()
                     public static {{retType}} Prop => {{baseExpr}};
                 }
                 """;
-            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90);
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
             comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
 
             var ilVerifyMessage = (useValueTask, useGeneric) switch
@@ -7282,5 +7283,437 @@ static void validate(ModuleSymbol m)
                     m.GlobalNamespace.GetMember("Test1.<M2>d__0.x").GetAttributes().Select(a => a.ToString()));
             }
         }
+
+        [Fact]
+        public void RuntimeAsync_CompilerFeatureFlag_EnabledWithRuntimeAsync()
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                await Task.CompletedTask;
+                """;
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("<Main>$", "0xa") });
+
+            verifier.VerifyIL("<top-level-statements-entry-point>", """
+                {
+                  // Code size       11 (0xb)
+                  .maxstack  1
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ret
+                }
+                """);
+        }
+
+        [Fact]
+        public void RuntimeAsync_CompilerFeatureFlag_EnabledWithoutRuntimeAsync()
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                await Task.CompletedTask;
+                """;
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
+
+            var verifier = CompileAndVerify(comp, verify: Verification.FailsPEVerify);
+
+            verifier.VerifyIL("<top-level-statements-entry-point>", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<<Main>$>d__0 V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<<Main>$>d__0.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<<Main>$>d__0>(ref Program.<<Main>$>d__0)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+        }
+
+        [Theory]
+        [CombinatorialData]
+        public void RuntimeAsync_CompilerFeatureFlag_DisabledWithRuntimeAsync(bool explicitDisable)
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                await Task.CompletedTask;
+                """;
+
+            var parseOptions = TestOptions.RegularPreview;
+            if (explicitDisable)
+            {
+                parseOptions = parseOptions.WithFeature("runtime-async", "off");
+            }
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers], targetFramework: TargetFramework.Net90, parseOptions: parseOptions);
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.FailsPEVerify);
+
+            verifier.VerifyIL("<top-level-statements-entry-point>", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<<Main>$>d__0 V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<<Main>$>d__0.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<<Main>$>d__0>(ref Program.<<Main>$>d__0)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>$>d__0.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+        }
+
+        [Theory]
+        [CombinatorialData]
+        public void RuntimeAsync_CompilerFeatureFlag_DisabledWithRuntimeAsync_EnabledOnMethod(bool explicitDisable)
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(true)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var parseOptions = TestOptions.RegularPreview;
+            if (explicitDisable)
+            {
+                parseOptions = parseOptions.WithFeature("runtime-async", "off");
+            }
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: parseOptions);
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("Main", "0xa") });
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       11 (0xb)
+                  .maxstack  1
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ret
+                }
+                """);
+        }
+
+        [Theory]
+        [CombinatorialData]
+        public void RuntimeAsync_CompilerFeatureFlag_DisabledWithRuntimeAsync_EnabledOnMethod_NoPreferenceOnNestedLocalFunction(bool explicitDisable)
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(true)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+
+                        async Task LocalFunc() => await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var parseOptions = TestOptions.RegularPreview;
+            if (explicitDisable)
+            {
+                parseOptions = parseOptions.WithFeature("runtime-async", "off");
+            }
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: parseOptions);
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("Main", "0xa") });
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       11 (0xb)
+                  .maxstack  1
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ret
+                }
+                """);
+
+            verifier.VerifyIL("Program.<Main>g__LocalFunc|0_0()", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<<Main>g__LocalFunc|0_0>d V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>g__LocalFunc|0_0>d.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<<Main>g__LocalFunc|0_0>d.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>g__LocalFunc|0_0>d.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<<Main>g__LocalFunc|0_0>d>(ref Program.<<Main>g__LocalFunc|0_0>d)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<<Main>g__LocalFunc|0_0>d.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+        }
+
+        [Theory]
+        [CombinatorialData]
+        public void RuntimeAsync_CompilerFeatureFlag_DisabledWithRuntimeAsync_EnabledOnMethod_NoPreferenceOnNestedLambda(bool explicitDisable)
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(true)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+
+                        var a = async () => await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var parseOptions = TestOptions.RegularPreview;
+            if (explicitDisable)
+            {
+                parseOptions = parseOptions.WithFeature("runtime-async", "off");
+            }
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: parseOptions);
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("Main", "0x26") });
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       39 (0x27)
+                  .maxstack  2
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ldsfld     "System.Func<System.Threading.Tasks.Task> Program.<>c.<>9__0_0"
+                  IL_000f:  brtrue.s   IL_0026
+                  IL_0011:  ldsfld     "Program.<>c Program.<>c.<>9"
+                  IL_0016:  ldftn      "System.Threading.Tasks.Task Program.<>c.<Main>b__0_0()"
+                  IL_001c:  newobj     "System.Func<System.Threading.Tasks.Task>..ctor(object, nint)"
+                  IL_0021:  stsfld     "System.Func<System.Threading.Tasks.Task> Program.<>c.<>9__0_0"
+                  IL_0026:  ret
+                }
+                """);
+
+            verifier.VerifyIL("Program.<>c.<Main>b__0_0()", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<>c.<<Main>b__0_0>d V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<>c.<<Main>b__0_0>d.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<>c.<<Main>b__0_0>d.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<>c.<<Main>b__0_0>d.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<>c.<<Main>b__0_0>d>(ref Program.<>c.<<Main>b__0_0>d)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<>c.<<Main>b__0_0>d.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+        }
+
+        [Fact]
+        public void RuntimeAsync_CompilerFeatureFlag_EnabledWithRuntimeAsync_DisabledOnMethod()
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(false)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.FailsPEVerify);
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<Main>d__0 V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<Main>d__0.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<Main>d__0>(ref Program.<Main>d__0)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+        }
+
+        [Fact]
+        public void RuntimeAsync_CompilerFeatureFlag_EnabledWithRuntimeAsync_DisabledOnMethod_NoPreferenceOnNestedLocalFunction()
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(false)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+
+                        async Task LocalFunc() => await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("<Main>g__LocalFunc|0_0", "0xa") });
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<Main>d__0 V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<Main>d__0.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<Main>d__0>(ref Program.<Main>d__0)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+
+            verifier.VerifyIL("Program.<Main>g__LocalFunc|0_0()", """
+                {
+                  // Code size       11 (0xb)
+                  .maxstack  1
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ret
+                }
+                """);
+        }
+
+        [Fact]
+        public void RuntimeAsync_CompilerFeatureFlag_EnabledWithRuntimeAsync_DisabledOnMethod_NoPreferenceOnNestedLambda()
+        {
+            var source = """
+                using System.Threading.Tasks;
+
+                class Program
+                {
+                    [System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute(false)]
+                    static async Task Main()
+                    {
+                        await Task.CompletedTask;
+
+                        var a = async () => await Task.CompletedTask;
+                    }
+                }
+                """;
+
+            var comp = CreateCompilation([source, RuntimeAsyncAwaitHelpers, RuntimeAsyncMethodGenerationAttributeDefinition], targetFramework: TargetFramework.Net90, parseOptions: WithRuntimeAsync(TestOptions.RegularPreview));
+            comp.Assembly.SetOverrideRuntimeSupportsAsyncMethods();
+
+            var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("<Main>b__0_0", "0xa") });
+
+            verifier.VerifyIL("Program.Main()", """
+                {
+                  // Code size       47 (0x2f)
+                  .maxstack  2
+                  .locals init (Program.<Main>d__0 V_0)
+                  IL_0000:  ldloca.s   V_0
+                  IL_0002:  call       "System.Runtime.CompilerServices.AsyncTaskMethodBuilder System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create()"
+                  IL_0007:  stfld      "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_000c:  ldloca.s   V_0
+                  IL_000e:  ldc.i4.m1
+                  IL_000f:  stfld      "int Program.<Main>d__0.<>1__state"
+                  IL_0014:  ldloca.s   V_0
+                  IL_0016:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_001b:  ldloca.s   V_0
+                  IL_001d:  call       "void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Program.<Main>d__0>(ref Program.<Main>d__0)"
+                  IL_0022:  ldloca.s   V_0
+                  IL_0024:  ldflda     "System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<Main>d__0.<>t__builder"
+                  IL_0029:  call       "System.Threading.Tasks.Task System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Task.get"
+                  IL_002e:  ret
+                }
+                """);
+
+            verifier.VerifyIL("Program.<>c.<Main>b__0_0()", """
+                {
+                  // Code size       11 (0xb)
+                  .maxstack  1
+                  IL_0000:  call       "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"
+                  IL_0005:  call       "void System.Runtime.CompilerServices.RuntimeHelpers.Await(System.Threading.Tasks.Task)"
+                  IL_000a:  ret
+                }
+                """);
+        }
     }
 }
diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs
index 6b16eac1db4e2..606b084ca8c40 100644
--- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs
+++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs
@@ -491,5 +491,6 @@ static AttributeDescription()
         internal static readonly AttributeDescription CollectionBuilderAttribute = new AttributeDescription("System.Runtime.CompilerServices", "CollectionBuilderAttribute", s_signaturesOfCollectionBuilderAttribute);
         internal static readonly AttributeDescription OverloadResolutionPriorityAttribute = new AttributeDescription("System.Runtime.CompilerServices", "OverloadResolutionPriorityAttribute", s_signatures_HasThis_Void_Int32_Only);
         internal static readonly AttributeDescription CompilerLoweringPreserveAttribute = new AttributeDescription("System.Runtime.CompilerServices", "CompilerLoweringPreserveAttribute", s_signatures_HasThis_Void_Only);
+        internal static readonly AttributeDescription RuntimeAsyncMethodGenerationAttribute = new AttributeDescription("System.Runtime.CompilerServices", "RuntimeAsyncMethodGenerationAttribute", s_signatures_HasThis_Void_Boolean_Only);
     }
 }
diff --git a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs
index 97f3a40347ec0..a5d7b845c62f1 100644
--- a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs
+++ b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs
@@ -813,6 +813,13 @@ public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter
             }
             """;
 
+        internal const string RuntimeAsyncMethodGenerationAttributeDefinition = """
+            namespace System.Runtime.CompilerServices;
+
+            [AttributeUsage(AttributeTargets.Method)]
+            public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute();
+            """;
+
         protected static T GetSyntax<T>(SyntaxTree tree, string text)
         {
             return GetSyntaxes<T>(tree, text).Single();


Reviews:
user: login: RikkiGibson
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T22:09:17Z
user: login: 333fred
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T01:00:19Z
user: login: 333fred
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T01:00:59Z
user: login: jcouv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:44:50Z
user: login: jcouv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:51:32Z
user: login: jcouv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:55:58Z
user: login: jcouv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:03:38Z
user: login: jcouv
body: Done with review pass (iteration 3). Tests not looked at yet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:06:16Z
user: login: 333fred
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:08:04Z
user: login: 333fred
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:09:10Z
user: login: jcouv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:17:56Z
user: login: jcouv
body: LGTM Thanks (iteration 4)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T18:26:45Z

Review comments:
diff_hunk: @@ -15,6 +15,7 @@
 using Basic.Reference.Assemblies;
 using System.Runtime.CompilerServices;
 using System.Reflection;
+using ICSharpCode.Decompiler.IL;
path: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
user: login: RikkiGibson
body: intentional?
created_at: 2025-03-13T22:00:59Z
author_association: MEMBER
diff_hunk: @@ -15,6 +15,7 @@
 using Basic.Reference.Assemblies;
 using System.Runtime.CompilerServices;
 using System.Reflection;
+using ICSharpCode.Decompiler.IL;
path: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
user: login: 333fred
body: Nope, thanks.
created_at: 2025-03-14T01:00:19Z
author_association: MEMBER
diff_hunk: @@ -15,6 +15,7 @@
 using Basic.Reference.Assemblies;
 using System.Runtime.CompilerServices;
 using System.Reflection;
+using ICSharpCode.Decompiler.IL;
path: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs
user: login: 333fred
body: ```suggestion
```
created_at: 2025-03-14T01:00:58Z
author_association: MEMBER
diff_hunk: @@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
+            };
path: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
user: login: jcouv
body: Let's update the compiler spec for how those three flags (runtime feature flag, compiler feature flag, method-level attribute) work together
created_at: 2025-03-18T18:44:50Z
author_association: MEMBER
diff_hunk: @@ -149,5 +149,21 @@ public UnmanagedCallersOnlyAttributeData? UnmanagedCallersOnlyAttributeData
                 SetDataStored();
             }
         }
+
+        private ThreeState _hasRuntimeAsyncMethodGenerationAttribute;
+        public ThreeState HasRuntimeAsyncMethodGenerationAttribute
path: src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs
user: login: jcouv
body: >HasRuntimeAsyncMethodGenerationAttribute [](http://example.com/codeflow?start=26&length=40)

nit: the name is a bit misleading, as `ThreeState.False` is returned on a method that has the attribute (with `false` argument). 
Consider `RuntimeAsyncMethodGenerationSetting` (similar to another `ThreeState` attribute data that we have: `AssemblyDelaySignAttributeSetting`) #Closed
created_at: 2025-03-18T18:51:31Z
author_association: MEMBER
diff_hunk: @@ -661,6 +669,15 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut
             }
         }
 
+        internal ThreeState IsRuntimeAsyncEnabledInMethod
+        {
+            get
+            {
+                var data = GetDecodedWellKnownAttributeData();
path: src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs
user: login: jcouv
body: nit: consider inlining like other properties below
nit: for other similar properties, it looks like we use the same name as the underlying attribute data property. Consider using matching names too
```
internal ThreeState RuntimeAsyncMethodGenerationSetting => 
    GetDecodedWellKnownAttributeData()?.RuntimeAsyncMethodGenerationSetting ?? ThreeState.Unknown;
```
 #Closed
created_at: 2025-03-18T18:55:58Z
author_association: MEMBER
diff_hunk: @@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
path: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
user: login: jcouv
body: It looks like the default (on a new runtime) is still to generate the old state machine. Is that what we want?
I think the default should be to use runtime-async when the runtime supports it and there is no other gesture. That behavior is closer to the behavior we'll want after preview period.
Then you can make other gestures to control that more: 1. you can opt-out at method level with attribute, or 2. you can opt-out globally then restore only the methods that you want.
created_at: 2025-03-18T19:03:38Z
author_association: MEMBER
diff_hunk: @@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
path: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
user: login: 333fred
body: This isn't what I remember agreeing on though: from what I recall, the feature flag will be there in .NET 10. We do not want to turn this on by default in .NET 10, so explicit opt-in is needed.
created_at: 2025-03-19T18:08:04Z
author_association: MEMBER
diff_hunk: @@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
+            };
path: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
user: login: 333fred
body: Will update in main after this PR.
created_at: 2025-03-19T18:09:10Z
author_association: MEMBER
diff_hunk: @@ -327,8 +325,12 @@ internal bool IsRuntimeAsyncEnabledIn(MethodSymbol method)
                 return false;
             }
 
-            // PROTOTYPE: Check for attributes that turn on/off the feature member-by-member
-            return true;
+            return method switch
+            {
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true,
+                SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false,
+                _ => Feature("runtime-async") == "on"
path: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
user: login: jcouv
body: Ah, you're right. Given that we're not using a forked runtime and that we'll merge the preview compiler feature, we need an explicit opt-in... Thanks
created_at: 2025-03-19T18:17:56Z
author_association: MEMBER

PR comments:
user: login: 333fred
created_at: 2025-03-13T17:07:27Z
body: @jcouv @RikkiGibson for reviews please.
author_association: MEMBER
user: login: jcouv
created_at: 2025-03-18T18:31:08Z
body: >             if (!Assembly.RuntimeSupportsAsyncMethods)

Just to confirm, my understanding is that the flag will be present in .NET 10, but marked with [Experimental]. Is that correct?

---
Refers to: src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs:323 in cd76c3b. [](commit_id = cd76c3bc308d9ffd65d508c7272c34a42773e02e, deletion_comment = False)
author_association: MEMBER
user: login: 333fred
created_at: 2025-03-18T18:39:57Z
body: > Just to confirm, my understanding is that the flag will be present in .NET 10, but marked with [Experimental]. Is that correct?

That's my understanding as well. Also `RequiresPreviewFeatures`.
author_association: MEMBER