From 411e30d9e9544a3503d7a2e18ced0c653654767d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 17:43:36 -0500 Subject: [PATCH] crossgen2: allow struct/unmanaged-constrained generics over primitives in the version bubble ComputeInstantiationVersionsWithCode treated a primitive instantiation argument as versioning with the generic only when the generic parameter had no type constraints at all. C# encodes `where T : struct` and `where T : unmanaged` as a System.ValueType type constraint, so such instantiations (e.g. S) were neither precompiled nor inlined ("cannot get method info") without --inputbubble/--opt-cross-module. Ignore System.ValueType constraints for primitive arguments; interface and class constraints still disqualify. Also teach CrossModuleInliningInfoSection to name local inlinees that have no compiled body from the image metadata, and add an R2R test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5ac7e42-b3b6-4fe3-bf0b-f40970e8fae0 --- ...gen2-compilation-structure-enhancements.md | 4 +- .../StructConstrainedGenerics.cs | 44 +++++++++++++++++++ .../TestCases/R2RTestSuites.cs | 31 +++++++++++++ .../ReadyToRunCompilationModuleGroupBase.cs | 17 +++++-- .../CrossModuleInliningInfoSection.cs | 18 ++++++-- 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/PrimitiveInstantiations/StructConstrainedGenerics.cs diff --git a/docs/design/features/crossgen2-compilation-structure-enhancements.md b/docs/design/features/crossgen2-compilation-structure-enhancements.md index 2c6676100db214..5a8ecb31434389 100644 --- a/docs/design/features/crossgen2-compilation-structure-enhancements.md +++ b/docs/design/features/crossgen2-compilation-structure-enhancements.md @@ -7,14 +7,16 @@ This document describes the concept of a version bubble, how to specify them to ## Behavior of code that shares a version bubble 1. Inlining is only permitted within a version bubble unless the method is marked with `System.Runtime.Versioning.NonVersionableAttribute`. If the inlinee is marked as NonVersionable, it may ALWAYS be inlined into the method being compiled. -2. Generic instantiations may only be ahead of time compiled into the application if the definition of the generic is defined within the version bubble as well as the instantiation arguments to the generic. As an exception to the requirement for the instantiation arguments to be defined within the bubble, the shared generic type `System.__Canon` will always be considered to be part of the version bubble. Also, the list of very well known types (object, string, int, uint, short, ushort, byte, sbyte, long, ulong, float, double, IntPtr, and UIntPtr) are also considered to be part of the version bubble as long as the generic is not constrained on an interface or class. For Example +2. Generic instantiations may only be ahead of time compiled into the application if the definition of the generic is defined within the version bubble as well as the instantiation arguments to the generic. As an exception to the requirement for the instantiation arguments to be defined within the bubble, the shared generic type `System.__Canon` will always be considered to be part of the version bubble. Also, the list of very well known types (object, string, int, uint, short, ushort, byte, sbyte, long, ulong, float, double, IntPtr, and UIntPtr) are also considered to be part of the version bubble as long as the generic is not constrained on an interface or class. The `struct` and `unmanaged` constraints, which C# encodes as a `System.ValueType` type constraint, do not count as class constraints for primitive instantiations. For Example ```csharp class MyGeneric {} class ConstrainedGeneric where T : IEquatable {} +class StructGeneric where T : struct {} ... //MyGeneric would be always be in the version bubble where MyGeneric was defined. //ConstrainedGeneric would only be in the version bubble of ConstrainedGeneric if ConstrainedGeneric shared a version bubble with System.Private.CoreLib. +//StructGeneric would always be in the version bubble where StructGeneric was defined. //MyGeneric would would only be in the version bubble of MyGeneric if MyGeneric shared a version bubble with System.Private.CoreLib. //ConstrainedGeneric would would only be in the version bubble of ConstrainedGeneric if ConstrainedGeneric shared a version bubble with System.Private.CoreLib. ``` diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/PrimitiveInstantiations/StructConstrainedGenerics.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/PrimitiveInstantiations/StructConstrainedGenerics.cs new file mode 100644 index 00000000000000..3ed8b040dac7c5 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/PrimitiveInstantiations/StructConstrainedGenerics.cs @@ -0,0 +1,44 @@ +// 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; + +public struct StructConstrainedBox where T : struct +{ + private T _value; + + public StructConstrainedBox(T value) => _value = value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public int GetValueHashCode() => _value.GetHashCode(); +} + +public static class StructConstrainedGenerics +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int StructConstrained(T value) where T : struct => value.GetHashCode(); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int UnmanagedConstrained(T value) where T : unmanaged => value.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InlineableStructConstrained(T value) where T : struct => value is char ? 1 : 2; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InlineableUnmanagedConstrained(T value) where T : unmanaged => value is char ? 1 : 2; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallStructConstrained(int value) => StructConstrained(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallUnmanagedConstrained(int value) => UnmanagedConstrained(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallStructConstrainedBox(int value) => new StructConstrainedBox(value).GetValueHashCode(); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallInlineableStructConstrained(char value) => InlineableStructConstrained(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int CallInlineableUnmanagedConstrained(char value) => InlineableUnmanagedConstrained(value); +} 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 55728597715585..669cb6d6e5ef2b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -145,6 +145,37 @@ static void Validate(ReadyToRunReader reader) } } + [Fact] + public void StructConstrainedPrimitiveInstantiations() + { + var structConstrainedGenerics = new CompiledAssembly + { + AssemblyName = nameof(StructConstrainedPrimitiveInstantiations), + SourceResourceNames = ["PrimitiveInstantiations/StructConstrainedGenerics.cs"], + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(StructConstrainedPrimitiveInstantiations), + [ + new(nameof(StructConstrainedPrimitiveInstantiations), [new CrossgenAssembly(structConstrainedGenerics)]) + { + Options = [Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + string diag; + + Assert.True(R2RAssert.HasCompiledMethod(reader, "StructConstrainedGenerics", "StructConstrained", out diag, ["int"]), diag); + Assert.True(R2RAssert.HasCompiledMethod(reader, "StructConstrainedGenerics", "UnmanagedConstrained", out diag, ["int"]), diag); + Assert.True(R2RAssert.HasCompiledMethod(reader, "StructConstrainedBox`1", "GetValueHashCode", out diag), diag); + Assert.True(R2RAssert.HasInlinedMethod(reader, "CallInlineableStructConstrained", "InlineableStructConstrained", out diag), diag); + Assert.True(R2RAssert.HasInlinedMethod(reader, "CallInlineableUnmanagedConstrained", "InlineableUnmanagedConstrained", out diag), diag); + } + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))] public void WasmWebcilModule() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs index 14a95095b5cca8..4bd4861d99fd16 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs @@ -946,6 +946,15 @@ private static bool ComputeInstantiationVersionsWithCode(Instantiation inst, Typ { if (genericParam.HasReferenceTypeConstraint) return false; + + // C# encodes the 'struct' and 'unmanaged' constraints as a System.ValueType type constraint. + // Every primitive satisfies it, and it exposes nothing beyond what an unconstrained + // type parameter already can, so only other (interface or class) constraints disqualify. + foreach (TypeDesc constraint in genericParam.TypeConstraints) + { + if (!constraint.IsWellKnownType(WellKnownType.ValueType)) + return false; + } } else { @@ -955,11 +964,11 @@ private static bool ComputeInstantiationVersionsWithCode(Instantiation inst, Typ if (instType.IsString && genericParam.HasDefaultConstructorConstraint) return false; - } - // This checks to see if the type constraints list is empty - if (genericParam.TypeConstraints.GetEnumerator().MoveNext()) - return false; + // This checks to see if the type constraints list is empty + if (genericParam.TypeConstraints.GetEnumerator().MoveNext()) + return false; + } } else { diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs index bd788cd500d525..503b9688c27b71 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/CrossModuleInliningInfoSection.cs @@ -272,9 +272,21 @@ private string ResolveLocalMethod(uint rid, uint moduleIndex) { _localMethodMap ??= BuildLocalMethodMap(); - return _localMethodMap.TryGetValue((moduleIndex, rid), out string name) - ? name - : $""; + if (_localMethodMap.TryGetValue((moduleIndex, rid), out string name)) + return name; + + // Inlinees that have no compiled body of their own (e.g. AggressiveInlining helpers) are not in the + // map; name them from the image's own metadata when it isn't a composite. + if (!_r2r.Composite && moduleIndex == 0 && + _r2r.GetGlobalMetadata()?.MetadataReader is MetadataReader mdReader && + rid <= (uint)mdReader.GetTableRowCount(TableIndex.MethodDef)) + { + MethodDefinition methodDef = mdReader.GetMethodDefinition(MetadataTokens.MethodDefinitionHandle((int)rid)); + TypeDefinition typeDef = mdReader.GetTypeDefinition(methodDef.GetDeclaringType()); + return $"{mdReader.GetString(typeDef.Name)}.{mdReader.GetString(methodDef.Name)}"; + } + + return $""; } private ReadyToRunImportSection FindILBodyImportSection()