From 229e802e191914b79832c1a5a526b0dab1524ae8 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 30 Jul 2026 15:44:10 -0700 Subject: [PATCH 1/4] Support enum values in substitutions Allow ILLink to parse numeric enum values and NativeAOT to resolve names for int-backed enums. Add shared coverage for fields and stubbed method returns. Assisted-by: GitHub Copilot:gpt-5.6-sol code-review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77531f73-7423-457e-891c-705990f7e7af --- .../Compiler/BodySubstitutionParser.cs | 21 +++++ .../TestCases/TestSuites.cs | 1 + .../Linker.Steps/ProcessLinkerXmlBase.cs | 2 + .../Substitutions/EnumSubstitutions.cs | 84 +++++++++++++++++++ .../Substitutions/EnumSubstitutions.xml | 10 +++ 5 files changed, 118 insertions(+) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.xml diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/BodySubstitutionParser.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/BodySubstitutionParser.cs index 0d4a575f362b6f..70d55feb6d9b28 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/BodySubstitutionParser.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/BodySubstitutionParser.cs @@ -7,6 +7,7 @@ using System.IO; using System.Reflection.Metadata; using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; using System.Xml; using System.Xml.XPath; using System.Globalization; @@ -163,6 +164,26 @@ private static MethodDesc FindMethod(TypeDesc type, string signature) private static object TryCreateSubstitution(TypeDesc type, string value) { + if (type.IsEnum && type.UnderlyingType.Category == TypeFlags.Int32) + { + foreach (FieldDesc field in type.GetFields()) + { + if (field.IsStatic && + field.Name.StringEquals(value) && + field is EcmaField ecmaField) + { + MetadataReader reader = ecmaField.MetadataReader; + ConstantHandle constantHandle = reader.GetFieldDefinition(ecmaField.Handle).GetDefaultValue(); + if (!constantHandle.IsNil) + { + Constant constant = reader.GetConstant(constantHandle); + if (constant.TypeCode == ConstantTypeCode.Int32) + return reader.GetBlobReader(constant.Value).ReadInt32(); + } + } + } + } + switch (type.UnderlyingType.Category) { case TypeFlags.Int32: diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TestSuites.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TestSuites.cs index 86d94be1f14c7c..8bf26c389adb35 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TestSuites.cs @@ -109,6 +109,7 @@ public void Substitutions(string t) { switch (t) { + case "EnumSubstitutions": case "FeatureGuardSubstitutions": Run(t); break; diff --git a/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs b/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs index 3d7bb2b2f2d6d9..73e0c75ed17e19 100644 --- a/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs +++ b/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs @@ -650,6 +650,8 @@ public bool TryConvertValue(string value, TypeReference target, out object? resu result = Convert.ToInt32(enumField.Constant); return true; } + + return TryConvertValue(value, typeDefinition.GetEnumUnderlyingType(), out result); } break; diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs new file mode 100644 index 00000000000000..54cb951c398ddf --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.CompilerServices; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.Substitutions +{ + [ExpectedNoWarnings] + [SetupLinkerSubstitutionFile("EnumSubstitutions.xml")] + [IgnoreSubstitutions(false)] + [SkipKeptItemsValidation(By = Tool.NativeAot)] + public class EnumSubstitutions + { + [Kept] + [KeptMember("value__")] + [KeptBaseType(typeof(Enum))] + private enum SubstitutionValue + { + [Kept] + Initial, + + [Kept] + ByName, + + [Kept] + ByNumber, + } + + [Kept] + private static readonly SubstitutionValue FieldByName = SubstitutionValue.Initial; + + [Kept] + private static readonly SubstitutionValue FieldByNumber = SubstitutionValue.Initial; + + public static void Main() + { + IsFieldByNameExpected(); + IsFieldByNumberExpected(); + MethodByName(); + MethodByNumber(); + } + + [Kept] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool IsFieldByNameExpected() + { + return FieldByName == SubstitutionValue.ByName; + } + + [Kept] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool IsFieldByNumberExpected() + { + return FieldByNumber == SubstitutionValue.ByNumber; + } + + [Kept] + [ExpectedInstructionSequence(new[] { + "ldc.i4 0x1", + "ret", + })] + [ExpectedLocalsSequence(new string[0])] + [MethodImpl(MethodImplOptions.NoInlining)] + private static SubstitutionValue MethodByName() + { + return SubstitutionValue.Initial; + } + + [Kept] + [ExpectedInstructionSequence(new[] { + "ldc.i4 0x2", + "ret", + })] + [ExpectedLocalsSequence(new string[0])] + [MethodImpl(MethodImplOptions.NoInlining)] + private static SubstitutionValue MethodByNumber() + { + return SubstitutionValue.Initial; + } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.xml b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.xml new file mode 100644 index 00000000000000..f632582a3d2f99 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.xml @@ -0,0 +1,10 @@ + + + + + + + + + + From 3f671cf914a4c7350aa20001216ceb853d88fd00 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 31 Jul 2026 10:34:42 -0700 Subject: [PATCH 2/4] Validate enum field substitutions after trimming Assisted-by: GitHub Copilot:gpt-5.6-sol [code-review] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77531f73-7423-457e-891c-705990f7e7af --- .../Substitutions/EnumSubstitutions.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs index 54cb951c398ddf..79628c73db3148 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs @@ -37,24 +37,34 @@ private enum SubstitutionValue public static void Main() { - IsFieldByNameExpected(); - IsFieldByNumberExpected(); + TestFieldByName(); + TestFieldByNumber(); MethodByName(); MethodByNumber(); } [Kept] - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool IsFieldByNameExpected() + [ExpectBodyModified] + private static void TestFieldByName() + { + if (FieldByName != SubstitutionValue.ByName) + FieldByNameIncorrect(); + } + + private static void FieldByNameIncorrect() { - return FieldByName == SubstitutionValue.ByName; } [Kept] - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool IsFieldByNumberExpected() + [ExpectBodyModified] + private static void TestFieldByNumber() + { + if (FieldByNumber != SubstitutionValue.ByNumber) + FieldByNumberIncorrect(); + } + + private static void FieldByNumberIncorrect() { - return FieldByNumber == SubstitutionValue.ByNumber; } [Kept] From d3cb501dec443c946df3aa6ce509a9ffa7bb8762 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 31 Jul 2026 11:16:06 -0700 Subject: [PATCH 3/4] Verify linked enum field substitutions exactly Assisted-by: GitHub Copilot:gpt-5.6-sol [code-review] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77531f73-7423-457e-891c-705990f7e7af --- .../Substitutions/EnumSubstitutions.cs | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs index 79628c73db3148..43b2ef866b8ac2 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Substitutions/EnumSubstitutions.cs @@ -37,33 +37,55 @@ private enum SubstitutionValue public static void Main() { - TestFieldByName(); - TestFieldByNumber(); + VerifyFieldByNameSubstitution(); + VerifyFieldByNumberSubstitution(); MethodByName(); MethodByNumber(); } [Kept] - [ExpectBodyModified] - private static void TestFieldByName() + [ExpectedInstructionSequence(new[] { + "nop", + "ldsfld Mono.Linker.Tests.Cases.Substitutions.EnumSubstitutions/SubstitutionValue Mono.Linker.Tests.Cases.Substitutions.EnumSubstitutions::FieldByName", + "ldc.i4.1", + "ceq", + "ldc.i4.0", + "ceq", + "stloc.0", + "ldloc.0", + "brfalse.s il_10", + "ret", + })] + private static void VerifyFieldByNameSubstitution() { if (FieldByName != SubstitutionValue.ByName) - FieldByNameIncorrect(); + ReachableOnUnexpectedFieldByNameValue(); } - private static void FieldByNameIncorrect() + private static void ReachableOnUnexpectedFieldByNameValue() { } [Kept] - [ExpectBodyModified] - private static void TestFieldByNumber() + [ExpectedInstructionSequence(new[] { + "nop", + "ldsfld Mono.Linker.Tests.Cases.Substitutions.EnumSubstitutions/SubstitutionValue Mono.Linker.Tests.Cases.Substitutions.EnumSubstitutions::FieldByNumber", + "ldc.i4.2", + "ceq", + "ldc.i4.0", + "ceq", + "stloc.0", + "ldloc.0", + "brfalse.s il_10", + "ret", + })] + private static void VerifyFieldByNumberSubstitution() { if (FieldByNumber != SubstitutionValue.ByNumber) - FieldByNumberIncorrect(); + ReachableOnUnexpectedFieldByNumberValue(); } - private static void FieldByNumberIncorrect() + private static void ReachableOnUnexpectedFieldByNumberValue() { } From 633ebdc269b5b212da89a04bf9982f3966eac6d3 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 31 Jul 2026 12:06:39 -0700 Subject: [PATCH 4/4] Limit enum numeric substitutions to Int32 Assisted-by: GitHub Copilot:gpt-5.6-sol [code-review] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77531f73-7423-457e-891c-705990f7e7af --- .../illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs b/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs index 73e0c75ed17e19..30a55be0105813 100644 --- a/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs +++ b/src/tools/illink/src/linker/Linker.Steps/ProcessLinkerXmlBase.cs @@ -651,7 +651,9 @@ public bool TryConvertValue(string value, TypeReference target, out object? resu return true; } - return TryConvertValue(value, typeDefinition.GetEnumUnderlyingType(), out result); + var underlyingType = typeDefinition.GetEnumUnderlyingType(); + if (underlyingType.MetadataType == MetadataType.Int32) + return TryConvertValue(value, underlyingType, out result); } break;