diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
index 1f545ceb58fe59..989feb34240e5f 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Reflection.PortableExecutable;
using ILCompiler.ReadyToRun.Tests.TestCasesRunner;
using ILCompiler.Reflection.ReadyToRun;
@@ -432,6 +433,61 @@ static void Validate(ReadyToRunReader reader)
}
}
+ ///
+ /// #129813 / PR #129884: crossgen2 --strip-il-bodies must preserve the IL of non-async
+ /// Task/ValueTask-returning methods, which is needed to compile the runtime-async variant.
+ ///
+ [Fact]
+ public void RuntimeAsyncStripILBodiesPreservesTaskReturningIL()
+ {
+ var stripILBodies = new CompiledAssembly
+ {
+ AssemblyName = nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL),
+ SourceResourceNames =
+ [
+ "RuntimeAsync/StripILBodies.cs",
+ "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs",
+ ],
+ Features = { RuntimeAsyncFeature },
+ };
+
+ new R2RTestRunner(_output).Run(new R2RTestCase(
+ nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL),
+ [
+ new(nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL), [new CrossgenAssembly(stripILBodies)])
+ {
+ Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize, Crossgen2Option.StripILBodies],
+ Validate = Validate,
+ },
+ ]));
+
+ static void Validate(ReadyToRunReader reader)
+ {
+ string diag;
+
+ string componentFile = Path.Combine(
+ Path.GetDirectoryName(reader.Filename)!,
+ nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL) + ".dll");
+
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncTaskOfTForwarder", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncValueTaskOfTForwarder", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncTaskForwarder", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncValueTaskForwarder", out diag), diag);
+
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "GenericIdentity", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsPresent(componentFile, "GenericHolder`1", "MethodOnGenericType", out diag), diag);
+
+ Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "PlainStrippableMethod", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "ComputeTag", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "Root", out diag), diag);
+
+ Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "AsyncTaskMethod", out diag), diag);
+ Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "AsyncValueTaskMethod", out diag), diag);
+ Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncTaskMethod", out diag), diag);
+ Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncValueTaskMethod", out diag), diag);
+ }
+ }
+
///
/// PR #123643: Async methods capturing GC refs across await points
/// produce ContinuationLayout fixups encoding the GC ref map.
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/StripILBodies.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/StripILBodies.cs
new file mode 100644
index 00000000000000..714129287fa7ef
--- /dev/null
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/StripILBodies.cs
@@ -0,0 +1,79 @@
+// Test: crossgen2 --strip-il-bodies IL preservation.
+// Validates that non-async Task/ValueTask-returning methods, generic methods,
+// and methods on generic types keep their IL, while plain and async methods are stripped.
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+
+public static class StripILBodies
+{
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static Task SyncTaskOfTForwarder(int value)
+ {
+ return Task.FromResult(value + 1);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static ValueTask SyncValueTaskOfTForwarder(int value)
+ {
+ return new ValueTask(value + 2);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static Task SyncTaskForwarder()
+ {
+ return Task.CompletedTask;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static ValueTask SyncValueTaskForwarder()
+ {
+ return default;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T GenericIdentity(T value)
+ {
+ return value;
+ }
+
+ public static class GenericHolder
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static int MethodOnGenericType(int a, int b)
+ {
+ return a + b;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static async Task AsyncTaskMethod()
+ {
+ await Task.Yield();
+ return 42;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static async ValueTask AsyncValueTaskMethod()
+ {
+ await Task.Yield();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static int PlainStrippableMethod(int a, int b)
+ {
+ return a + b + ComputeTag();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static int ComputeTag()
+ {
+ return 12345;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static int Root()
+ {
+ return GenericIdentity(1) + GenericHolder.MethodOnGenericType(2, 3);
+ }
+}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs
index 1cc08276b0eaa7..a359f7bfa45d05 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs
@@ -33,6 +33,7 @@ internal enum Crossgen2Option
HotColdSplitting,
Optimize,
TargetArchArm,
+ StripILBodies,
}
internal static class Crossgen2OptionsExtensions
@@ -59,6 +60,7 @@ internal static class Crossgen2OptionsExtensions
Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting",
Crossgen2Option.Optimize => $"--optimize",
Crossgen2Option.TargetArchArm => $"--targetarch:arm",
+ Crossgen2Option.StripILBodies => $"--strip-il-bodies",
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs
index 3d6ec148e25b6e..86c3112a2992ba 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs
@@ -1019,6 +1019,81 @@ public static bool HasCompiledMethod(ReadyToRunReader reader, string declaringTy
$"All compiled methods ({allMethods.Count}):\n {string.Join("\n ", allMethods.Select(m => $"{m.DeclaringType}:{m.Name}"))}";
return false;
}
+
+ ///
+ /// Reads the raw IL byte stream of a method definition from a component MSIL file.
+ ///
+ private static bool TryGetMethodIL(string msilFilePath, string declaringType, string methodName, out byte[] il, out string diagnostic)
+ {
+ il = Array.Empty();
+
+ if (!File.Exists(msilFilePath))
+ {
+ diagnostic = $"Component MSIL file not found: '{msilFilePath}'.";
+ return false;
+ }
+
+ using var fileStream = new FileStream(msilFilePath, FileMode.Open, FileAccess.Read);
+ using var peReader = new PEReader(fileStream);
+ MetadataReader mr = peReader.GetMetadataReader();
+ foreach (TypeDefinitionHandle typeHandle in mr.TypeDefinitions)
+ {
+ TypeDefinition type = mr.GetTypeDefinition(typeHandle);
+ if (mr.GetString(type.Name) != declaringType)
+ continue;
+
+ foreach (MethodDefinitionHandle methodHandle in type.GetMethods())
+ {
+ MethodDefinition method = mr.GetMethodDefinition(methodHandle);
+ if (mr.GetString(method.Name) != methodName)
+ continue;
+
+ int rva = method.RelativeVirtualAddress;
+ if (rva == 0)
+ {
+ diagnostic = $"Method '{declaringType}.{methodName}' has no IL body (RVA 0).";
+ return false;
+ }
+
+ il = peReader.GetMethodBody(rva).GetILBytes() ?? Array.Empty();
+ diagnostic = string.Empty;
+ return true;
+ }
+ }
+
+ diagnostic = $"Method '{declaringType}.{methodName}' not found in '{msilFilePath}'.";
+ return false;
+ }
+
+ ///
+ /// Returns true if the method's IL body was stripped by crossgen2 (--strip-il-bodies).
+ ///
+ public static bool MethodILIsStripped(string msilFilePath, string declaringType, string methodName, out string diagnostic)
+ {
+ if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic))
+ return false;
+
+ bool stripped = il.AsSpan().SequenceEqual((ReadOnlySpan)[0x14, 0x7A]);
+ diagnostic = stripped
+ ? $"IL of '{declaringType}.{methodName}' is stripped (ldnull; throw)."
+ : $"Expected IL of '{declaringType}.{methodName}' to be stripped, but it is present ({il.Length} bytes: {BitConverter.ToString(il)}).";
+ return stripped;
+ }
+
+ ///
+ /// Returns true if the method's full IL body is present in the component MSIL file.
+ ///
+ public static bool MethodILIsPresent(string msilFilePath, string declaringType, string methodName, out string diagnostic)
+ {
+ if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic))
+ return false;
+
+ bool present = !il.AsSpan().SequenceEqual((ReadOnlySpan)[0x14, 0x7A]);
+ diagnostic = present
+ ? $"IL of '{declaringType}.{methodName}' is present ({il.Length} bytes)."
+ : $"Expected IL of '{declaringType}.{methodName}' to be present, but it was stripped (ldnull; throw).";
+ return present;
+ }
}
///