From 50c77f3487a8f5f03962c2204fc2d2f8684c272a Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 1 Jul 2026 13:34:51 +0200 Subject: [PATCH 1/2] Add crossgen2 --strip-il-bodies runtime-async regression test Adds an ILCompiler.ReadyToRun.Tests case that crossgen2-composites a small runtime-async assembly with --strip-il-bodies and inspects the emitted component MSIL to assert every branch of the strip decision, covering the regression from #129813 fixed by #129884. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 56 +++++++++++++ .../TestCases/RuntimeAsync/StripILBodies.cs | 79 +++++++++++++++++++ .../TestCasesRunner/R2RDriver.cs | 2 + .../TestCasesRunner/R2RResultChecker.cs | 75 ++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/StripILBodies.cs 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..eba974eb8c6cd1 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([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([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; + } } /// From 3e66e9b5f1cd5b638ddafbe73f464d80d21b40dd Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Thu, 2 Jul 2026 10:16:03 +0200 Subject: [PATCH 2/2] Fix CS1929: cast collection expression to ReadOnlySpan for SequenceEqual The untyped [0x14, 0x7A] collection expression prevented the MemoryExtensions.SequenceEqual overload from binding, causing the CLR_Tools_Tests leg to fail to compile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestCasesRunner/R2RResultChecker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 eba974eb8c6cd1..86c3112a2992ba 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -1073,7 +1073,7 @@ public static bool MethodILIsStripped(string msilFilePath, string declaringType, if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic)) return false; - bool stripped = il.AsSpan().SequenceEqual([0x14, 0x7A]); + 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)})."; @@ -1088,7 +1088,7 @@ public static bool MethodILIsPresent(string msilFilePath, string declaringType, if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic)) return false; - bool present = !il.AsSpan().SequenceEqual([0x14, 0x7A]); + 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).";