From 37800921afe698ee9ee1aa972b710fe85c10f1e7 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 25 Sep 2026 21:38:27 -0500 Subject: [PATCH 1/9] Fix crossgen2 overflow emitting inlining info for async-variant inliners When a runtime-async variant of a cross-module generic method is compiled into an image with --opt-cross-module and inlines another method, CompileMethod records a Check_IL_Body fixup for the AsyncMethodVariant. InliningInfoNode collapsed every inliner to its EcmaMethod and looked up that method's Check_IL_Body import instead, which was never marked, so its index was unset and the checked cast threw OverflowException (or asserted in debug builds). Keep the identity whose IL body fixup was recorded for cross-module inliners while still deduplicating by EcmaMethod, and use the primary EcmaMethod when encoding RIDs. Fixes #134015 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AsyncGenericInlinerConsumer.cs | 20 ++++++++ .../AsyncCrossModuleGenericLib.cs | 28 +++++++++++ .../TestCases/R2RTestSuites.cs | 48 +++++++++++++++++++ .../ReadyToRun/InliningInfoNode.cs | 42 +++++++++------- 4 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs new file mode 100644 index 00000000000000..1eac162a1a7916 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +/// +/// Consumer that awaits a runtime-async method on a generic type from AsyncCrossModuleGenericLib, +/// instantiated over a value type defined in this assembly so that the async variant of +/// AsyncGenericWrapper<LocalAsyncStruct>.InvokeGetValueAsync() is compiled into this image. +/// + +public struct LocalAsyncStruct { public int Value; } + +public static class AsyncGenericInlinerConsumer +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task UseAsync() + { + var wrapper = new AsyncGenericWrapper(new LocalAsyncStruct { Value = 1 }); + return await wrapper.InvokeGetValueAsync(); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs new file mode 100644 index 00000000000000..926192590fec12 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +/// +/// Library with a generic type whose runtime-async method inlines a utility method. +/// When the consumer awaits the method on an instantiation over one of its own value types, +/// crossgen2 compiles the async variant of the method into the consumer as a CrossModuleCompileable +/// generic, making the async variant a cross-module inliner of AsyncGenericUtility.GetAsyncGenericValue. +/// +public static class AsyncGenericUtility +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetAsyncGenericValue() => 42; +} + +public class AsyncGenericWrapper +{ + private T _value; + + public AsyncGenericWrapper(T value) => _value = value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task InvokeGetValueAsync() + { + await Task.Yield(); + return AsyncGenericUtility.GetAsyncGenericValue(); + } +} 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 827907a5aa43f3..015e1b64cffb47 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -1995,6 +1995,54 @@ static void Validate(ReadyToRunReader reader) } } + /// + /// Tests cross-module generic compilation where the runtime-async variant of a method from an + /// --opt-cross-module library is compiled into the consumer and inlines another library method. + /// The inlining info must reference the IL body fixup that was recorded for the async variant. + /// + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsNotWasmTarget))] + public void AsyncCrossModuleGenericInliner() + { + var asyncCrossModuleGenericLib = new CompiledAssembly + { + AssemblyName = "AsyncCrossModuleGenericLib", + SourceResourceNames = ["CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs"], + Features = { RuntimeAsyncFeature }, + }; + var consumer = new CompiledAssembly + { + AssemblyName = "AsyncGenericInlinerConsumer", + SourceResourceNames = ["CrossModuleInlining/AsyncGenericInlinerConsumer.cs"], + References = [asyncCrossModuleGenericLib], + Features = { RuntimeAsyncFeature }, + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(AsyncCrossModuleGenericInliner), + [ + new(consumer.AssemblyName, + [ + new CrossgenAssembly(asyncCrossModuleGenericLib) + { + Kind = Crossgen2InputKind.Reference, + Options = [Crossgen2AssemblyOption.CrossModuleOptimization], + }, + new CrossgenAssembly(consumer), + ]) + { + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + Assert.True(R2RAssert.HasManifestRef(reader, "AsyncCrossModuleGenericLib", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInliners(reader, "GetAsyncGenericValue", ["InvokeGetValueAsync"], out diag), diag); + } + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsNotWasmTarget))] public void VirtualMethodGenericsNonGVM() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs index ab07c61ce11c58..c5adaf3d9c74a0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs @@ -60,7 +60,10 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) if (relocsOnly) return new ObjectData(Array.Empty(), Array.Empty(), 1, new ISymbolDefinitionNode[] { this }); - Dictionary> inlineeToInliners = new Dictionary>(); + // Maps each inlinee to its inliners. Inliners are keyed by their metadata definition; the value is the + // method identity whose IL body fixup was recorded when the inliner was compiled, which differs from + // the metadata definition for runtime-async variants. + Dictionary> inlineeToInliners = new Dictionary>(); // Build a map from inlinee to the list of inliners // We are only interested in the generic definitions of these. @@ -73,6 +76,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } MethodDesc inliner = methodNode.Method; EcmaMethod inlinerDefinition = (EcmaMethod)inliner.GetPrimaryMethodDesc().GetTypicalMethodDefinition(); + MethodDesc inlinerTypicalDefinition = inliner.GetTypicalMethodDefinition(); + MethodDesc inlinerIdentity = inlinerTypicalDefinition is AsyncMethodVariant ? inlinerTypicalDefinition : inlinerDefinition; if (inlinerDefinition.IsNonVersionable()) { @@ -123,12 +128,19 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } } - if (!inlineeToInliners.TryGetValue(inlineeDefinition, out HashSet inliners)) + if (!inlineeToInliners.TryGetValue(inlineeDefinition, out Dictionary inliners)) { - inliners = new HashSet(); + inliners = new Dictionary(); inlineeToInliners.Add(inlineeDefinition, inliners); } - inliners.Add(inlinerDefinition); + + // Both the task-returning and the async variant of a method may inline the same inlinee. + // Report the method once, preferring the task-returning definition so the result does + // not depend on the order in which compiled methods are enumerated. + if (!inliners.TryGetValue(inlinerDefinition, out MethodDesc existingInliner) || existingInliner is not EcmaMethod) + { + inliners[inlinerDefinition] = inlinerIdentity; + } } } @@ -176,12 +188,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) sig.Append(new UnsignedConstant((uint)factory.ManifestMetadataTable.ModuleToIndex(ecmaInlinee.Module))); } - // We're only concerned with metadata here, so we can convert all to EcmaMethod and lose info about AsyncVariant vs Task-Returning - List sortedInliners = new List(inlineeWithInliners.Value.Count); - foreach (var inliner in inlineeWithInliners.Value) - { - sortedInliners.Add((EcmaMethod)inliner.GetPrimaryMethodDesc()); - } + // We're only concerned with metadata here, so we can use the EcmaMethod keys and lose info about AsyncVariant vs Task-Returning + List sortedInliners = new List(inlineeWithInliners.Value.Keys); sortedInliners.MergeSort((a, b) => { if (a == b) @@ -242,8 +250,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) bool isCrossModuleInlinee = !factory.CompilationModuleGroup.VersionsWithMethodBody(inlinee); Debug.Assert(!isCrossModuleInlinee || factory.CompilationModuleGroup.CrossModuleInlineable(inlinee)); - EcmaMethod[] sortedInliners = new EcmaMethod[inlineeWithInliners.Value.Count]; - inlineeWithInliners.Value.CopyTo(sortedInliners); + MethodDesc[] sortedInliners = new MethodDesc[inlineeWithInliners.Value.Count]; + inlineeWithInliners.Value.Values.CopyTo(sortedInliners, 0); sortedInliners.MergeSort((a, b) => { @@ -270,14 +278,16 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } else { - int aRid = MetadataTokens.GetRowNumber(a.Handle); - int bRid = MetadataTokens.GetRowNumber(b.Handle); + EcmaMethod ecmaA = (EcmaMethod)a.GetPrimaryMethodDesc(); + EcmaMethod ecmaB = (EcmaMethod)b.GetPrimaryMethodDesc(); + int aRid = MetadataTokens.GetRowNumber(ecmaA.Handle); + int bRid = MetadataTokens.GetRowNumber(ecmaB.Handle); if (aRid < bRid) return -1; else if (aRid > bRid) return 1; - result = a.Module.CompareTo(b.Module); + result = ecmaA.Module.CompareTo(ecmaB.Module); } Debug.Assert(result != 0); return result; @@ -336,7 +346,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) uint baseRid = 0; for (; inlinerIndex < sortedInliners.Length; inlinerIndex++) { - var inliner = sortedInliners[inlinerIndex]; + var inliner = (EcmaMethod)sortedInliners[inlinerIndex].GetPrimaryMethodDesc(); uint inlinerRid = (uint)MetadataTokens.GetRowNumber(inliner.Handle); uint ridDelta = inlinerRid - baseRid; baseRid = inlinerRid; From 692227c218bade2f78135c57f0ffcbade3b8cb62 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 25 Sep 2026 21:47:42 -0500 Subject: [PATCH 2/9] Never prefer a compiler-generated async thunk as the reported inliner Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReadyToRun/InliningInfoNode.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs index c5adaf3d9c74a0..a25a8415396c2b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs @@ -60,9 +60,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) if (relocsOnly) return new ObjectData(Array.Empty(), Array.Empty(), 1, new ISymbolDefinitionNode[] { this }); - // Maps each inlinee to its inliners. Inliners are keyed by their metadata definition; the value is the - // method identity whose IL body fixup was recorded when the inliner was compiled, which differs from - // the metadata definition for runtime-async variants. + // Inliners are keyed by metadata definition; the value is the identity whose Check_IL_Body fixup was recorded when compiling it. Dictionary> inlineeToInliners = new Dictionary>(); // Build a map from inlinee to the list of inliners @@ -134,10 +132,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) inlineeToInliners.Add(inlineeDefinition, inliners); } - // Both the task-returning and the async variant of a method may inline the same inlinee. - // Report the method once, preferring the task-returning definition so the result does - // not depend on the order in which compiled methods are enumerated. - if (!inliners.TryGetValue(inlinerDefinition, out MethodDesc existingInliner) || existingInliner is not EcmaMethod) + // Both variants of a method may inline the same inlinee; report it once, independent of enumeration order. + if (!inliners.TryGetValue(inlinerDefinition, out MethodDesc existingInliner) || IsPreferredInliner(inlinerIdentity, existingInliner)) { inliners[inlinerDefinition] = inlinerIdentity; } @@ -384,6 +380,16 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) relocs: null, alignment: 8, definedSymbols: new ISymbolDefinitionNode[] { this }); + + // Compiler-generated async thunks cannot carry a Check_IL_Body fixup, so never prefer one. + static bool IsPreferredInliner(MethodDesc candidate, MethodDesc existing) + { + bool candidateIsThunk = candidate.IsCompilerGeneratedILBodyForAsync(); + if (candidateIsThunk != existing.IsCompilerGeneratedILBodyForAsync()) + return !candidateIsThunk; + + return candidate is EcmaMethod && existing is not EcmaMethod; + } } public override int ClassCode => -87382891; From 020d61fb810e9df02f20b26f6bc567bcaa4e2c4c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 25 Sep 2026 22:39:19 -0500 Subject: [PATCH 3/9] Skip async thunks as cross-module inliners and cover variant dedup Compiler-generated async thunks cannot carry a Check_IL_Body fixup, so don't report them as cross-module inliners. Extend the regression test so both the task-returning method and its async variant inline the same cross-module inlinee. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AsyncGenericInlinerConsumer.cs | 14 +++++++ .../AsyncCrossModuleGenericLib.cs | 10 +++++ .../TestCases/R2RTestSuites.cs | 2 + .../TestCasesRunner/R2RResultChecker.cs | 42 +++++++++++++++++++ .../ReadyToRun/InliningInfoNode.cs | 18 ++++---- 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs index 1eac162a1a7916..0baf255f811812 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs @@ -17,4 +17,18 @@ public static async Task UseAsync() var wrapper = new AsyncGenericWrapper(new LocalAsyncStruct { Value = 1 }); return await wrapper.InvokeGetValueAsync(); } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task AwaitValueTask() + { + var wrapper = new AsyncGenericWrapper(new LocalAsyncStruct { Value = 2 }); + return await wrapper.GetValueTask(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Task ReturnValueTask() + { + var wrapper = new AsyncGenericWrapper(new LocalAsyncStruct { Value = 3 }); + return wrapper.GetValueTask(); + } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs index 926192590fec12..80d28b73cc0589 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs @@ -11,6 +11,9 @@ public static class AsyncGenericUtility { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetAsyncGenericValue() => 42; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetSharedInlineeValue() => 43; } public class AsyncGenericWrapper @@ -25,4 +28,11 @@ public async Task InvokeGetValueAsync() await Task.Yield(); return AsyncGenericUtility.GetAsyncGenericValue(); } + + // Not async, so both the task-returning method and its async variant are compiled from this IL. + [MethodImpl(MethodImplOptions.NoInlining)] + public Task GetValueTask() + { + return Task.FromResult(AsyncGenericUtility.GetSharedInlineeValue()); + } } 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 015e1b64cffb47..1e4949ae0ae223 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -2040,6 +2040,8 @@ static void Validate(ReadyToRunReader reader) Assert.True(R2RAssert.HasManifestRef(reader, "AsyncCrossModuleGenericLib", out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliningInfo(reader, out diag), diag); Assert.True(R2RAssert.HasCrossModuleInliners(reader, "GetAsyncGenericValue", ["InvokeGetValueAsync"], out diag), diag); + Assert.True(R2RAssert.HasAsyncVariant(reader, "GetValueTask", out diag), diag); + Assert.True(R2RAssert.HasCrossModuleInlinerCount(reader, "GetSharedInlineeValue", "GetValueTask", 1, out diag), diag); } } 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 54324b56d65ef3..13f223874a38d8 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -637,6 +637,48 @@ public static bool HasCrossModuleInliners( return false; } + /// + /// Returns true if the CrossModuleInlineInfo entry for an inlinee matching + /// has exactly cross-module inliners whose resolved names contain + /// . + /// + public static bool HasCrossModuleInlinerCount( + ReadyToRunReader reader, + string inlineeMethodName, + string inlinerMethodName, + int expectedCount, + out string diagnostic) + { + if (!TryGetCrossModuleInliningInfoSection(reader, out var inliningInfo, out diagnostic)) + return false; + + foreach (var entry in inliningInfo.GetEntries()) + { + string inlineeName = inliningInfo.ResolveMethodName(entry.Inlinee); + if (!inlineeName.Contains(inlineeMethodName, StringComparison.OrdinalIgnoreCase)) + continue; + + var matchingInliners = new List(); + foreach (var inliner in entry.Inliners) + { + if (!inliner.IsCrossModule) + continue; + + string inlinerName = inliningInfo.ResolveMethodName(inliner); + if (inlinerName.Contains(inlinerMethodName, StringComparison.OrdinalIgnoreCase)) + matchingInliners.Add(inlinerName); + } + + diagnostic = + $"Inlinee '{inlineeName}': expected {expectedCount} cross-module inliner(s) matching '{inlinerMethodName}', " + + $"found {matchingInliners.Count}:\n {string.Join("\n ", matchingInliners)}"; + return matchingInliners.Count == expectedCount; + } + + diagnostic = $"No CrossModuleInlineInfo entry found for inlinee matching '{inlineeMethodName}'."; + return false; + } + /// /// Returns true if any inlining info section (CrossModuleInlineInfo or InliningInfo2) records /// that inlined . diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs index a25a8415396c2b..7b6a06f5250762 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs @@ -86,6 +86,12 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) // Only encode inlining info for inliners within the active module, or if cross module inline format is in use Debug.Assert(AllowCrossModuleInlines || (inlinerDefinition.Module == _module)); + // Cross-module inliners are encoded by their Check_IL_Body import, which cannot describe a compiler-generated async thunk. + if (AllowCrossModuleInlines && inlinerIdentity.IsCompilerGeneratedILBodyForAsync() && !factory.CompilationModuleGroup.VersionsWithMethodBody(inlinerDefinition)) + { + continue; + } + bool inlinerReportAllVersionsWithInlinee = !AllowCrossModuleInlines || factory.CompilationModuleGroup.CrossModuleCompileable(inlinerDefinition); foreach (MethodDesc inlinee in inlinees) @@ -133,7 +139,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } // Both variants of a method may inline the same inlinee; report it once, independent of enumeration order. - if (!inliners.TryGetValue(inlinerDefinition, out MethodDesc existingInliner) || IsPreferredInliner(inlinerIdentity, existingInliner)) + if (!inliners.TryGetValue(inlinerDefinition, out MethodDesc existingInliner) || (inlinerIdentity is EcmaMethod && existingInliner is not EcmaMethod)) { inliners[inlinerDefinition] = inlinerIdentity; } @@ -380,16 +386,6 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) relocs: null, alignment: 8, definedSymbols: new ISymbolDefinitionNode[] { this }); - - // Compiler-generated async thunks cannot carry a Check_IL_Body fixup, so never prefer one. - static bool IsPreferredInliner(MethodDesc candidate, MethodDesc existing) - { - bool candidateIsThunk = candidate.IsCompilerGeneratedILBodyForAsync(); - if (candidateIsThunk != existing.IsCompilerGeneratedILBodyForAsync()) - return !candidateIsThunk; - - return candidate is EcmaMethod && existing is not EcmaMethod; - } } public override int ClassCode => -87382891; From eba14fc54234a30dab6b808395d30c508638e500 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 25 Sep 2026 22:55:10 -0500 Subject: [PATCH 4/9] Share the IL body fixup identity between CompileMethod and InliningInfoNode Only report a cross-module inliner when it has its own Check_IL_Body fixup, using the same identity CompileMethod creates the fixup for. Return-dropping thunks, resumption stubs, and other wrappers that unwrap to an EcmaMethod no longer borrow that method's import. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReadyToRun/ILBodyFixupSignature.cs | 9 +++++++++ .../ReadyToRun/InliningInfoNode.cs | 15 ++++++++++----- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs index dccd0ee634dcf7..8fdae301435767 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ILBodyFixupSignature.cs @@ -51,6 +51,15 @@ public ILBodyFixupSignature(ReadyToRunFixupKind fixupKind, MethodDesc signatureM _signatureMethod = signatureMethod; } + /// + /// Returns the method a compiled method's own IL body fixup is created for, or null if the compiled method cannot have one. + /// + public static MethodDesc GetSignatureMethodForCompiledMethod(MethodDesc compiledMethod) + { + MethodDesc typicalDef = compiledMethod.GetTypicalMethodDefinition(); + return typicalDef is EcmaMethod or AsyncMethodVariant ? typicalDef : null; + } + public override int ClassCode => 308579267; protected override void OnMarked(NodeFactory context) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs index 7b6a06f5250762..aba01cb2ff70c9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs @@ -74,8 +74,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } MethodDesc inliner = methodNode.Method; EcmaMethod inlinerDefinition = (EcmaMethod)inliner.GetPrimaryMethodDesc().GetTypicalMethodDefinition(); - MethodDesc inlinerTypicalDefinition = inliner.GetTypicalMethodDefinition(); - MethodDesc inlinerIdentity = inlinerTypicalDefinition is AsyncMethodVariant ? inlinerTypicalDefinition : inlinerDefinition; + MethodDesc inlinerIdentity = ILBodyFixupSignature.GetSignatureMethodForCompiledMethod(inliner); if (inlinerDefinition.IsNonVersionable()) { @@ -86,10 +85,16 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) // Only encode inlining info for inliners within the active module, or if cross module inline format is in use Debug.Assert(AllowCrossModuleInlines || (inlinerDefinition.Module == _module)); - // Cross-module inliners are encoded by their Check_IL_Body import, which cannot describe a compiler-generated async thunk. - if (AllowCrossModuleInlines && inlinerIdentity.IsCompilerGeneratedILBodyForAsync() && !factory.CompilationModuleGroup.VersionsWithMethodBody(inlinerDefinition)) + if (AllowCrossModuleInlines && !factory.CompilationModuleGroup.VersionsWithMethodBody(inlinerDefinition)) { - continue; + // Cross-module inliners are encoded by their own Check_IL_Body import. Thunks and stubs don't have one. + if (inlinerIdentity is null || inlinerIdentity.IsCompilerGeneratedILBodyForAsync()) + continue; + } + else + { + // Inliners in the version bubble are encoded by metadata RID. + inlinerIdentity = inlinerDefinition; } bool inlinerReportAllVersionsWithInlinee = !AllowCrossModuleInlines || factory.CompilationModuleGroup.CrossModuleCompileable(inlinerDefinition); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 7c79992e58a49f..9c1116e0135f8f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -825,7 +825,7 @@ public void CompileMethod(MethodWithGCInfo methodCodeNodeNeedingCode, Logger log } var typicalDef = MethodBeingCompiled.GetTypicalMethodDefinition(); - if (typicalDef is EcmaMethod or AsyncMethodVariant) + if (ILBodyFixupSignature.GetSignatureMethodForCompiledMethod(MethodBeingCompiled) is not null) { var ecmaMethod = (EcmaMethod)typicalDef.GetPrimaryMethodDesc(); if ((methodIL.GetMethodILScopeDefinition() is IEcmaMethodIL && _compilation.SymbolNodeFactory.VerifyTypeAndFieldLayout && ecmaMethod.Module == typicalDef.Context.SystemModule) || From f1d85b45c6547cd60605614b74e6ad554be66a07 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 19:25:45 -0500 Subject: [PATCH 5/9] Add license header to AsyncCrossModuleGenericLib.cs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Dependencies/AsyncCrossModuleGenericLib.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs index 80d28b73cc0589..850d2988bc984a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/Dependencies/AsyncCrossModuleGenericLib.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.CompilerServices; using System.Threading.Tasks; From e4a292cc71582e10ffb49ff12aa25f326ee28d67 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 19:25:57 -0500 Subject: [PATCH 6/9] Add license comments to AsyncGenericInlinerConsumer.cs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../CrossModuleInlining/AsyncGenericInlinerConsumer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs index 0baf255f811812..78659f10998b72 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CrossModuleInlining/AsyncGenericInlinerConsumer.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.CompilerServices; using System.Threading.Tasks; From ef4ce5f932e818bf0bef4083611da50009441216 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 21:24:49 -0500 Subject: [PATCH 7/9] Detect runtime-async and synchronized flag changes in IL body fixups Check_IL_Body compared only the IL bytes, EH clauses, locals and referenced metadata, so a method whose miAsync (or miSynchronized) impl flag changed while its IL stayed the same still matched, and stale cross-module inlined code was used. Record those impl flags in the byte that describes the locals table, on both the crossgen2 and VM sides. Methods without these flags produce the same blob as before. Also reject the fixup when the runtime method has no IL header, such as the task-returning thunk of a method that became runtime-async, instead of reading it. Add a readytorun test that compiles against a helper with cross-module inlining and runs against a version whose methods have identical IL but opposite runtime-async flags. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/coreclr/botr/readytorun-format.md | 2 +- .../ReadyToRunStandaloneMethodMetadata.cs | 18 ++-- .../vm/readytorunstandalonemethodmetadata.cpp | 25 +++-- .../async-ilbody-versioning.cs | 47 ++++++++ .../async-ilbody-versioning.csproj | 101 ++++++++++++++++++ .../async-ilbody-versioning/helper.cs | 32 ++++++ .../helperv1/helper.csproj | 11 ++ .../helperv2/helper.csproj | 12 +++ 8 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs create mode 100644 src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj create mode 100644 src/tests/readytorun/async-ilbody-versioning/helper.cs create mode 100644 src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj create mode 100644 src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index f1abb56d88e643..01cc573c15745f 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -336,7 +336,7 @@ ECMA 335 does not have a natural encoding for describing an overridden method. T #### IL Body signatures -ECMA 335 does not define a format that can represent the exact implementation of a method by itself. This signature holds all of the IL of the method, the EH table, the locals table, and each token (other than type references) in those tables is replaced with an index into a local stream of signatures. Those signatures are simply verbatim copies of the needed metadata to describe MemberRefs, TypeSpecs, MethodSpecs, StandaloneSignatures and strings. All of that is bundled into a large byte array. In addition, a series of TypeSignatures follows which allow the type references to be resolved, as well as a methodreference to the uninstantiated method. Assuming all of this matches with the data that is present at runtime, the fixup is considered to be satisfied. See ReadyToRunStandaloneMetadata.cs for the exact details of the format. +ECMA 335 does not define a format that can represent the exact implementation of a method by itself. This signature holds all of the IL of the method, the EH table, the locals table, and each token (other than type references) in those tables is replaced with an index into a local stream of signatures. Those signatures are simply verbatim copies of the needed metadata to describe MemberRefs, TypeSpecs, MethodSpecs, StandaloneSignatures and strings. All of that is bundled into a large byte array. The byte that describes the locals table also records the method implementation flags that change how the same IL executes: `0x04` is set for `miAsync` (runtime-async) methods and `0x08` for `miSynchronized` methods. In addition, a series of TypeSignatures follows which allow the type references to be resolved, as well as a methodreference to the uninstantiated method. Assuming all of this matches with the data that is present at runtime, the fixup is considered to be satisfied. See ReadyToRunStandaloneMethodMetadata.cs for the exact details of the format. #### InjectStringThunks signatures diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs index 14432b03f0f704..ea18df026999b3 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -92,14 +93,17 @@ public static ReadyToRunStandaloneMethodMetadata Compute(EcmaMethod wrappedMetho } } - if (localsBlob.Length == 0) - { - // No locals. Encode a 2 to indicate this - _nonCodeAlternateBlob.WriteByte(2); - } - else + // The impl flags bits record flags that change how the same IL executes. + byte localsAndImplFlags = localsBlob.Length == 0 ? (byte)2 : (_methodBody.LocalVariablesInitialized ? (byte)1 : (byte)0); + MethodImplAttributes implAttributes = metadataReader.GetMethodDefinition(wrappedMethod.Handle).ImplAttributes; + if ((implAttributes & MethodImplAttributes.Async) != 0) + localsAndImplFlags |= 4; + if ((implAttributes & MethodImplAttributes.Synchronized) != 0) + localsAndImplFlags |= 8; + _nonCodeAlternateBlob.WriteByte(localsAndImplFlags); + + if (localsBlob.Length != 0) { - _nonCodeAlternateBlob.WriteByte(_methodBody.LocalVariablesInitialized ? (byte)1 : (byte)0); EcmaSignatureTranslator sigTranslator = new EcmaSignatureTranslator(localsBlob, _nonCodeAlternateBlob, GetAlternateStreamToken); sigTranslator.ParseLocalsSignature(); } diff --git a/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp b/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp index 7daef864c1ea83..9ce71a67a9c672 100644 --- a/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp +++ b/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp @@ -19,6 +19,7 @@ class ReadyToRunStandaloneMethodMetadataHelper MapSHash alternateTokens; Module* pModule; IMDInternalImport* pMDImport; + DWORD dwImplFlags; public: @@ -27,8 +28,11 @@ class ReadyToRunStandaloneMethodMetadataHelper currentILStreamIterator(0), pTypeRefTokenStream(pTypeRefTokenStreamInput), pModule(pMD->GetModule()), - pMDImport(pMD->GetMDImport()) + pMDImport(pMD->GetMDImport()), + dwImplFlags(0) { + IfFailThrow(pMDImport->GetMethodImplProps(pMD->GetMemberDef(), NULL, &dwImplFlags)); + { // Fill IL stream with initial data byte* ilStreamData = ilStream.OpenRawBuffer(header.CodeSize); @@ -63,13 +67,16 @@ class ReadyToRunStandaloneMethodMetadataHelper } } - if (header.cbLocalVarSig == 0) - { - nonCodeAlternateBlob.AppendByte(2); - } - else + // The impl flags bits record flags that change how the same IL executes. + uint8_t localsAndImplFlags = (header.cbLocalVarSig == 0) ? 2 : ((header.Flags & CorILMethod_InitLocals) ? 1 : 0); + if (IsMiAsync(dwImplFlags)) + localsAndImplFlags |= 4; + if (IsMiSynchronized(dwImplFlags)) + localsAndImplFlags |= 8; + nonCodeAlternateBlob.AppendByte(localsAndImplFlags); + + if (header.cbLocalVarSig != 0) { - nonCodeAlternateBlob.AppendByte((header.Flags & CorILMethod_InitLocals) ? 1 : 0); SigParser localSigParser(header.LocalVarSig, header.cbLocalVarSig); StandaloneSigTranslator sigTranslator(&localSigParser, &nonCodeAlternateBlob, this); sigTranslator.ParseLocalsSignature(); @@ -666,6 +673,10 @@ ReadyToRunStandaloneMethodMetadata* GetReadyToRunStandaloneMethodMetadata(Method { ReadyToRunStandaloneMethodMetadata* retVal; + // For example, the task-returning thunk of a runtime-async method has no IL body to compare. + if (!pMD->HasILHeader()) + return NULL; + { CrstHolder lock(&s_csReadyToRunStandaloneMethodMetadata); if (s_methodMetadata->Lookup(pMD, &retVal)) diff --git a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs b/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs new file mode 100644 index 00000000000000..7573b75e136040 --- /dev/null +++ b/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +// This assembly is compiled with cross-module inlining against V1 of helper.dll and runs against V2, +// whose methods have the same IL bodies but opposite runtime-async impl flags. Code that inlined the +// V1 bodies must be rejected, so every result below must reflect the V2 semantics. +public class AsyncILBodyVersioning +{ + static bool s_failed; + + static void Check(string testName, bool passed) + { + if (!passed) + { + Console.WriteLine($" FAILED: {testName}"); + s_failed = true; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task AwaitBecomesAsync() => await AsyncFlagHelper.BecomesAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static Task CallBecomesAsync() => AsyncFlagHelper.BecomesAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task AwaitStopsBeingAsync() => await AsyncFlagHelper.StopsBeingAsync(); + + static async Task RunTests() + { + Check("AwaitBecomesAsync", await AwaitBecomesAsync() is Task); + Check("CallBecomesAsync", await CallBecomesAsync() is Task); + Check("AwaitStopsBeingAsync", await AwaitStopsBeingAsync() is null); + } + + public static int Main() + { + RunTests().GetAwaiter().GetResult(); + + Console.WriteLine(s_failed ? "FAILED" : "PASSED"); + return s_failed ? 1 : 100; + } +} diff --git a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj b/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj new file mode 100644 index 00000000000000..6e381b5547a5b4 --- /dev/null +++ b/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj @@ -0,0 +1,101 @@ + + + true + false + false + true + $(Features);runtime-async=on + + true + + true + true + + + + + + + + + + NUL +if not exist IL_DLLS\async-ilbody-versioning.dll ( + copy /y async-ilbody-versioning.dll IL_DLLS\async-ilbody-versioning.dll +) +if not exist IL_DLLS\async-ilbody-versioning.dll ( + echo FAILED to copy async-ilbody-versioning.dll to IL_DLLS + exit /b 1 +) + +REM Compile against V1 of the helper, with cross-module inlining of its methods +%Core_Root%\crossgen2\crossgen2.exe -O --opt-cross-module:helper -r:%Core_Root%\*.dll -r:%25CD%\helperv1\helper.dll -o:async-ilbody-versioning.dll IL_DLLS\async-ilbody-versioning.dll + +set CrossGenStatus=!ERRORLEVEL! +IF NOT !CrossGenStatus!==0 ( + ECHO Crossgen of async-ilbody-versioning.dll failed with exitcode - !CrossGenStatus! + Exit /b 1 +) + +REM Run against V2 of the helper +copy /y helperv2\helper.dll helper.dll + +endlocal +]]> + + + + + + $(OutputPath)/../helperv1/helper/ + $(OutputPath)/helperv1 + $(OutputPath)/../helperv2/helper/ + $(OutputPath)/helperv2 + + + + + + diff --git a/src/tests/readytorun/async-ilbody-versioning/helper.cs b/src/tests/readytorun/async-ilbody-versioning/helper.cs new file mode 100644 index 00000000000000..19094401c0fde8 --- /dev/null +++ b/src/tests/readytorun/async-ilbody-versioning/helper.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +#pragma warning disable CS1998 // Async method lacks 'await' operators + +// Each method has the same IL in V1 and V2; only the async impl flag differs. When the method is not +// runtime-async, awaiting it yields null. When it is runtime-async, the returned Task is the result. +public static class AsyncFlagHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if V2 + public static async Task BecomesAsync() +#else + public static Task BecomesAsync() +#endif + { + return Task.FromResult(null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if V2 + public static Task StopsBeingAsync() +#else + public static async Task StopsBeingAsync() +#endif + { + return Task.FromResult(null); + } +} diff --git a/src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj b/src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj new file mode 100644 index 00000000000000..8c97cd96c5e099 --- /dev/null +++ b/src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj @@ -0,0 +1,11 @@ + + + Library + $(Features);runtime-async=on + + true + + + + + diff --git a/src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj b/src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj new file mode 100644 index 00000000000000..d2ba216af62a4f --- /dev/null +++ b/src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj @@ -0,0 +1,12 @@ + + + Library + $(Features);runtime-async=on + $(DefineConstants);V2 + + true + + + + + From 957b3842aa380d473a47de616edd0e5d3643db94 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 29 Sep 2026 16:20:24 -0500 Subject: [PATCH 8/9] Name IL body flag values and cover the synchronized flag in the test Use named constants for the locals/impl-flags byte on both the crossgen2 and VM sides, and tighten the related comments. Rename the test to ilbody-implflags-versioning and add a case where a method becomes synchronized with an identical IL body. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReadyToRunStandaloneMethodMetadata.cs | 15 ++++++++---- .../vm/readytorunstandalonemethodmetadata.cpp | 21 ++++++++++++---- .../helper.cs | 17 ++++++++++--- .../helperv1/helper.csproj | 0 .../helperv2/helper.csproj | 0 .../ilbody-implflags-versioning.cs} | 16 ++++++++----- .../ilbody-implflags-versioning.csproj} | 24 +++++++++---------- 7 files changed, 63 insertions(+), 30 deletions(-) rename src/tests/readytorun/{async-ilbody-versioning => ilbody-implflags-versioning}/helper.cs (59%) rename src/tests/readytorun/{async-ilbody-versioning => ilbody-implflags-versioning}/helperv1/helper.csproj (100%) rename src/tests/readytorun/{async-ilbody-versioning => ilbody-implflags-versioning}/helperv2/helper.csproj (100%) rename src/tests/readytorun/{async-ilbody-versioning/async-ilbody-versioning.cs => ilbody-implflags-versioning/ilbody-implflags-versioning.cs} (61%) rename src/tests/readytorun/{async-ilbody-versioning/async-ilbody-versioning.csproj => ilbody-implflags-versioning/ilbody-implflags-versioning.csproj} (77%) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs index ea18df026999b3..c0c19a707696db 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunStandaloneMethodMetadata.cs @@ -20,6 +20,13 @@ namespace ILCompiler // That code can be found in src\coreclr\vm\readytorunstandalonemethodmetadata.cpp public class ReadyToRunStandaloneMethodMetadata { + // Values of the byte that follows the EH clauses. Must match the VM. + private const byte LocalsNotInitialized = 0; + private const byte LocalsInitialized = 1; + private const byte NoLocals = 2; + private const byte AsyncImplFlag = 4; + private const byte SynchronizedImplFlag = 8; + public byte[] ConstantData; public TypeDesc[] TypeRefs; @@ -93,13 +100,13 @@ public static ReadyToRunStandaloneMethodMetadata Compute(EcmaMethod wrappedMetho } } - // The impl flags bits record flags that change how the same IL executes. - byte localsAndImplFlags = localsBlob.Length == 0 ? (byte)2 : (_methodBody.LocalVariablesInitialized ? (byte)1 : (byte)0); + // Impl flags that change how the same IL executes are part of the IL body identity. + byte localsAndImplFlags = localsBlob.Length == 0 ? NoLocals : (_methodBody.LocalVariablesInitialized ? LocalsInitialized : LocalsNotInitialized); MethodImplAttributes implAttributes = metadataReader.GetMethodDefinition(wrappedMethod.Handle).ImplAttributes; if ((implAttributes & MethodImplAttributes.Async) != 0) - localsAndImplFlags |= 4; + localsAndImplFlags |= AsyncImplFlag; if ((implAttributes & MethodImplAttributes.Synchronized) != 0) - localsAndImplFlags |= 8; + localsAndImplFlags |= SynchronizedImplFlag; _nonCodeAlternateBlob.WriteByte(localsAndImplFlags); if (localsBlob.Length != 0) diff --git a/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp b/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp index 9ce71a67a9c672..e57d2b05bd2ba2 100644 --- a/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp +++ b/src/coreclr/vm/readytorunstandalonemethodmetadata.cpp @@ -3,6 +3,16 @@ #include "openum.h" #ifdef FEATURE_READYTORUN +// Values of the byte that follows the EH clauses. Must match CrossGen2. +enum : uint8_t +{ + ILBodyLocalsNotInitialized = 0, + ILBodyLocalsInitialized = 1, + ILBodyNoLocals = 2, + ILBodyAsyncImplFlag = 4, + ILBodySynchronizedImplFlag = 8, +}; + // Alternate form of metadata that represents a single method. Self contained except for type references // The behavior of this code must exactly match that of the ReadyToRunStandaloneMethodMetadata class in CrossGen2 // That code can be found in src\coreclr\tools\aot\ILCompiler.ReadyToRun\Compiler\ReadyToRunStandaloneMethodMetadata.cs @@ -67,12 +77,13 @@ class ReadyToRunStandaloneMethodMetadataHelper } } - // The impl flags bits record flags that change how the same IL executes. - uint8_t localsAndImplFlags = (header.cbLocalVarSig == 0) ? 2 : ((header.Flags & CorILMethod_InitLocals) ? 1 : 0); + // Impl flags that change how the same IL executes are part of the IL body identity. + uint8_t localsAndImplFlags = (header.cbLocalVarSig == 0) ? ILBodyNoLocals : + ((header.Flags & CorILMethod_InitLocals) ? ILBodyLocalsInitialized : ILBodyLocalsNotInitialized); if (IsMiAsync(dwImplFlags)) - localsAndImplFlags |= 4; + localsAndImplFlags |= ILBodyAsyncImplFlag; if (IsMiSynchronized(dwImplFlags)) - localsAndImplFlags |= 8; + localsAndImplFlags |= ILBodySynchronizedImplFlag; nonCodeAlternateBlob.AppendByte(localsAndImplFlags); if (header.cbLocalVarSig != 0) @@ -673,7 +684,7 @@ ReadyToRunStandaloneMethodMetadata* GetReadyToRunStandaloneMethodMetadata(Method { ReadyToRunStandaloneMethodMetadata* retVal; - // For example, the task-returning thunk of a runtime-async method has no IL body to compare. + // Thunks, such as the task-returning variant of a runtime-async method, have no IL body to compare. if (!pMD->HasILHeader()) return NULL; diff --git a/src/tests/readytorun/async-ilbody-versioning/helper.cs b/src/tests/readytorun/ilbody-implflags-versioning/helper.cs similarity index 59% rename from src/tests/readytorun/async-ilbody-versioning/helper.cs rename to src/tests/readytorun/ilbody-implflags-versioning/helper.cs index 19094401c0fde8..849883f021b2c8 100644 --- a/src/tests/readytorun/async-ilbody-versioning/helper.cs +++ b/src/tests/readytorun/ilbody-implflags-versioning/helper.cs @@ -2,14 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; #pragma warning disable CS1998 // Async method lacks 'await' operators -// Each method has the same IL in V1 and V2; only the async impl flag differs. When the method is not -// runtime-async, awaiting it yields null. When it is runtime-async, the returned Task is the result. -public static class AsyncFlagHelper +// Each method has the same IL in V1 and V2; only an impl flag differs. +public static class ImplFlagHelper { + // Awaiting a non-runtime-async method yields null; awaiting a runtime-async one yields the returned Task. [MethodImpl(MethodImplOptions.AggressiveInlining)] #if V2 public static async Task BecomesAsync() @@ -29,4 +30,14 @@ public static async Task StopsBeingAsync() { return Task.FromResult(null); } + +#if V2 + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.Synchronized)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool BecomesSynchronized() + { + return Monitor.IsEntered(typeof(ImplFlagHelper)); + } } diff --git a/src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj b/src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj similarity index 100% rename from src/tests/readytorun/async-ilbody-versioning/helperv1/helper.csproj rename to src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj diff --git a/src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj b/src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj similarity index 100% rename from src/tests/readytorun/async-ilbody-versioning/helperv2/helper.csproj rename to src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj diff --git a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs similarity index 61% rename from src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs rename to src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs index 7573b75e136040..6ea61fe67cc58d 100644 --- a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.cs +++ b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; // This assembly is compiled with cross-module inlining against V1 of helper.dll and runs against V2, -// whose methods have the same IL bodies but opposite runtime-async impl flags. Code that inlined the -// V1 bodies must be rejected, so every result below must reflect the V2 semantics. -public class AsyncILBodyVersioning +// whose methods have the same IL bodies but different runtime-async or synchronized impl flags. Code +// that inlined the V1 bodies must be rejected, so every result below must reflect the V2 semantics. +public class ILBodyImplFlagsVersioning { static bool s_failed; @@ -22,19 +22,23 @@ static void Check(string testName, bool passed) } [MethodImpl(MethodImplOptions.NoInlining)] - static async Task AwaitBecomesAsync() => await AsyncFlagHelper.BecomesAsync(); + static async Task AwaitBecomesAsync() => await ImplFlagHelper.BecomesAsync(); [MethodImpl(MethodImplOptions.NoInlining)] - static Task CallBecomesAsync() => AsyncFlagHelper.BecomesAsync(); + static Task CallBecomesAsync() => ImplFlagHelper.BecomesAsync(); [MethodImpl(MethodImplOptions.NoInlining)] - static async Task AwaitStopsBeingAsync() => await AsyncFlagHelper.StopsBeingAsync(); + static async Task AwaitStopsBeingAsync() => await ImplFlagHelper.StopsBeingAsync(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool CallBecomesSynchronized() => ImplFlagHelper.BecomesSynchronized(); static async Task RunTests() { Check("AwaitBecomesAsync", await AwaitBecomesAsync() is Task); Check("CallBecomesAsync", await CallBecomesAsync() is Task); Check("AwaitStopsBeingAsync", await AwaitStopsBeingAsync() is null); + Check("CallBecomesSynchronized", CallBecomesSynchronized()); } public static int Main() diff --git a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj similarity index 77% rename from src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj rename to src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj index 6e381b5547a5b4..6b5f40a36b8832 100644 --- a/src/tests/readytorun/async-ilbody-versioning/async-ilbody-versioning.csproj +++ b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj @@ -30,20 +30,20 @@ set "DOTNET_HeapVerify=" set "DOTNET_ReadyToRun=" md IL_DLLS 2>NUL -if not exist IL_DLLS\async-ilbody-versioning.dll ( - copy /y async-ilbody-versioning.dll IL_DLLS\async-ilbody-versioning.dll +if not exist IL_DLLS\ilbody-implflags-versioning.dll ( + copy /y ilbody-implflags-versioning.dll IL_DLLS\ilbody-implflags-versioning.dll ) -if not exist IL_DLLS\async-ilbody-versioning.dll ( - echo FAILED to copy async-ilbody-versioning.dll to IL_DLLS +if not exist IL_DLLS\ilbody-implflags-versioning.dll ( + echo FAILED to copy ilbody-implflags-versioning.dll to IL_DLLS exit /b 1 ) REM Compile against V1 of the helper, with cross-module inlining of its methods -%Core_Root%\crossgen2\crossgen2.exe -O --opt-cross-module:helper -r:%Core_Root%\*.dll -r:%25CD%\helperv1\helper.dll -o:async-ilbody-versioning.dll IL_DLLS\async-ilbody-versioning.dll +%Core_Root%\crossgen2\crossgen2.exe -O --opt-cross-module:helper -r:%Core_Root%\*.dll -r:%25CD%\helperv1\helper.dll -o:ilbody-implflags-versioning.dll IL_DLLS\ilbody-implflags-versioning.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( - ECHO Crossgen of async-ilbody-versioning.dll failed with exitcode - !CrossGenStatus! + ECHO Crossgen of ilbody-implflags-versioning.dll failed with exitcode - !CrossGenStatus! Exit /b 1 ) @@ -60,23 +60,23 @@ export -n DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun mkdir -p IL_DLLS -if [ ! -f IL_DLLS/async-ilbody-versioning.dll ] +if [ ! -f IL_DLLS/ilbody-implflags-versioning.dll ] then - cp async-ilbody-versioning.dll IL_DLLS/async-ilbody-versioning.dll + cp ilbody-implflags-versioning.dll IL_DLLS/ilbody-implflags-versioning.dll fi -if [ ! -f IL_DLLS/async-ilbody-versioning.dll ] +if [ ! -f IL_DLLS/ilbody-implflags-versioning.dll ] then - echo Failed to copy async-ilbody-versioning.dll to IL_DLLS + echo Failed to copy ilbody-implflags-versioning.dll to IL_DLLS exit 1 fi # Compile against V1 of the helper, with cross-module inlining of its methods -"$CORE_ROOT"/crossgen2/crossgen2 -O --opt-cross-module:helper -r:"$CORE_ROOT"/*.dll -r:helperv1/helper.dll -o:async-ilbody-versioning.dll IL_DLLS/async-ilbody-versioning.dll +"$CORE_ROOT"/crossgen2/crossgen2 -O --opt-cross-module:helper -r:"$CORE_ROOT"/*.dll -r:helperv1/helper.dll -o:ilbody-implflags-versioning.dll IL_DLLS/ilbody-implflags-versioning.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ] then - echo Crossgen of async-ilbody-versioning.dll failed with exitcode: $__cgExitCode + echo Crossgen of ilbody-implflags-versioning.dll failed with exitcode: $__cgExitCode exit 1 fi From b504d98a17a8c96dc26b56bd9c1ac417c911d51b Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 29 Sep 2026 20:16:35 -0500 Subject: [PATCH 9/9] Fix readytorun merged runner build and compute inliner identity lazily Give the ilbody-implflags-versioning helper a unique assembly name and exclude its versioned helper projects from the readytorun merged runner, as is already done for tests/testv1 and tests/testv2. Only compute the cross-module inliner identity when it is needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReadyToRun/InliningInfoNode.cs | 4 +++- .../helperv1/helper.csproj | 1 + .../helperv2/helper.csproj | 1 + .../ilbody-implflags-versioning.cs | 2 +- .../ilbody-implflags-versioning.csproj | 12 ++++++------ src/tests/readytorun/readytorun.csproj | 2 ++ 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs index aba01cb2ff70c9..8ce396d50f9359 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InliningInfoNode.cs @@ -74,7 +74,6 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) } MethodDesc inliner = methodNode.Method; EcmaMethod inlinerDefinition = (EcmaMethod)inliner.GetPrimaryMethodDesc().GetTypicalMethodDefinition(); - MethodDesc inlinerIdentity = ILBodyFixupSignature.GetSignatureMethodForCompiledMethod(inliner); if (inlinerDefinition.IsNonVersionable()) { @@ -85,8 +84,11 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) // Only encode inlining info for inliners within the active module, or if cross module inline format is in use Debug.Assert(AllowCrossModuleInlines || (inlinerDefinition.Module == _module)); + MethodDesc inlinerIdentity; if (AllowCrossModuleInlines && !factory.CompilationModuleGroup.VersionsWithMethodBody(inlinerDefinition)) { + inlinerIdentity = ILBodyFixupSignature.GetSignatureMethodForCompiledMethod(inliner); + // Cross-module inliners are encoded by their own Check_IL_Body import. Thunks and stubs don't have one. if (inlinerIdentity is null || inlinerIdentity.IsCompilerGeneratedILBodyForAsync()) continue; diff --git a/src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj b/src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj index 8c97cd96c5e099..ba6cee743157fd 100644 --- a/src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj +++ b/src/tests/readytorun/ilbody-implflags-versioning/helperv1/helper.csproj @@ -1,6 +1,7 @@ Library + ImplFlagHelper $(Features);runtime-async=on true diff --git a/src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj b/src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj index d2ba216af62a4f..b4e97d1520c277 100644 --- a/src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj +++ b/src/tests/readytorun/ilbody-implflags-versioning/helperv2/helper.csproj @@ -1,6 +1,7 @@ Library + ImplFlagHelper $(Features);runtime-async=on $(DefineConstants);V2 diff --git a/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs index 6ea61fe67cc58d..cbcc15626efc40 100644 --- a/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs +++ b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -// This assembly is compiled with cross-module inlining against V1 of helper.dll and runs against V2, +// This assembly is compiled with cross-module inlining against V1 of ImplFlagHelper.dll and runs against V2, // whose methods have the same IL bodies but different runtime-async or synchronized impl flags. Code // that inlined the V1 bodies must be rejected, so every result below must reflect the V2 semantics. public class ILBodyImplFlagsVersioning diff --git a/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj index 6b5f40a36b8832..6ee0398daec41f 100644 --- a/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj +++ b/src/tests/readytorun/ilbody-implflags-versioning/ilbody-implflags-versioning.csproj @@ -39,7 +39,7 @@ if not exist IL_DLLS\ilbody-implflags-versioning.dll ( ) REM Compile against V1 of the helper, with cross-module inlining of its methods -%Core_Root%\crossgen2\crossgen2.exe -O --opt-cross-module:helper -r:%Core_Root%\*.dll -r:%25CD%\helperv1\helper.dll -o:ilbody-implflags-versioning.dll IL_DLLS\ilbody-implflags-versioning.dll +%Core_Root%\crossgen2\crossgen2.exe -O --opt-cross-module:ImplFlagHelper -r:%Core_Root%\*.dll -r:%25CD%\helperv1\ImplFlagHelper.dll -o:ilbody-implflags-versioning.dll IL_DLLS\ilbody-implflags-versioning.dll set CrossGenStatus=!ERRORLEVEL! IF NOT !CrossGenStatus!==0 ( @@ -48,7 +48,7 @@ IF NOT !CrossGenStatus!==0 ( ) REM Run against V2 of the helper -copy /y helperv2\helper.dll helper.dll +copy /y helperv2\ImplFlagHelper.dll ImplFlagHelper.dll endlocal ]]> @@ -71,7 +71,7 @@ then fi # Compile against V1 of the helper, with cross-module inlining of its methods -"$CORE_ROOT"/crossgen2/crossgen2 -O --opt-cross-module:helper -r:"$CORE_ROOT"/*.dll -r:helperv1/helper.dll -o:ilbody-implflags-versioning.dll IL_DLLS/ilbody-implflags-versioning.dll +"$CORE_ROOT"/crossgen2/crossgen2 -O --opt-cross-module:ImplFlagHelper -r:"$CORE_ROOT"/*.dll -r:helperv1/ImplFlagHelper.dll -o:ilbody-implflags-versioning.dll IL_DLLS/ilbody-implflags-versioning.dll __cgExitCode=$? if [ $__cgExitCode -ne 0 ] @@ -81,7 +81,7 @@ then fi # Run against V2 of the helper -cp helperv2/helper.dll helper.dll +cp helperv2/ImplFlagHelper.dll ImplFlagHelper.dll export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun ]]> @@ -95,7 +95,7 @@ export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun $(OutputPath)/helperv2 - - + + diff --git a/src/tests/readytorun/readytorun.csproj b/src/tests/readytorun/readytorun.csproj index cb657444dd8d18..0b88400b5141d5 100644 --- a/src/tests/readytorun/readytorun.csproj +++ b/src/tests/readytorun/readytorun.csproj @@ -7,6 +7,8 @@ + +