From c30fd116d1b2913710d625f17329cb2820029a8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:51:16 +0000 Subject: [PATCH 1/9] Initial plan From dcf1d89d1c6d4b05f5855d3e42e2363d276d068b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:23:26 +0000 Subject: [PATCH 2/9] Fix TypeMapAssemblyTarget attribute removal when target assembly has no surviving entries Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com> --- src/coreclr/vm/assemblynative.cpp | 14 +++- .../src/linker/Linker/TypeMapHandler.cs | 83 ++++++++++++++----- .../TypeMapAllConditionalEntriesDep.cs | 17 ++++ .../TypeMapAllConditionalGroupDep.cs | 10 +++ ...emblyTargetRemovedWhenAllEntriesTrimmed.cs | 36 ++++++++ 5 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs diff --git a/src/coreclr/vm/assemblynative.cpp b/src/coreclr/vm/assemblynative.cpp index 97ee4dfc6dfbab..36c6d41e07e6e2 100644 --- a/src/coreclr/vm/assemblynative.cpp +++ b/src/coreclr/vm/assemblynative.cpp @@ -1624,7 +1624,19 @@ namespace AssemblySpec spec; spec.Init(assemblyNameString); - Assembly* pAssembly = spec.LoadAssembly(FILE_LOADED); + // Use fThrowOnFileNotFound=FALSE so that a missing assembly (e.g. trimmed away) is + // silently skipped rather than crashing the process with FileNotFoundException. + Assembly* pAssembly = spec.LoadAssembly(FILE_LOADED, FALSE); + + // If the assembly could not be found, skip it. This can happen when an assembly was + // referenced only through a TypeMapAssemblyTargetAttribute string name and was later + // trimmed away. The trimmer should have removed the attribute, but if it did not (e.g. + // in a non-trimmed scenario or due to a trimmer bug), we treat the missing assembly as + // contributing no entries rather than crashing the app. + if (pAssembly == NULL) + { + return TRUE; + } // Only add the assembly if it is unknown. if (_toProcess.Lookup(pAssembly) == NULL diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 2b1d2e4a55e8c4..cbf2d0164956ea 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -28,7 +28,16 @@ sealed class TypeMapHandler // [type map group: custom attributes] Dictionary> _pendingExternalTypeMapEntries = null!; Dictionary> _pendingProxyTypeMapEntries = null!; - Dictionary> _pendingAssemblyTargets = null!; + + // [type map group: (custom attribute, resolved target assembly)] + // The resolved target assembly is null if it could not be found. In that case, the attribute will not be marked. + Dictionary> _pendingAssemblyTargets = null!; + + // [target assembly: (type map group, custom attribute, calling method)] + // When a type map group is seen, assembly targets whose referenced assembly has not yet been marked are moved here. + // When the referenced assembly is eventually marked (due to a TypeMap entry being marked), all pending entries for + // it are also marked. + Dictionary> _pendingAssemblyTargetsByAssembly = null!; HashSet _referencedExternalTypeMaps = null!; HashSet _referencedProxyTypeMaps = null!; @@ -52,6 +61,7 @@ public void Initialize(LinkContext context, MarkStep markStep, AssemblyDefinitio _pendingExternalTypeMapEntries = new(typeReferenceEqualityComparer); _pendingProxyTypeMapEntries = new(typeReferenceEqualityComparer); _pendingAssemblyTargets = new(typeReferenceEqualityComparer); + _pendingAssemblyTargetsByAssembly = []; _referencedExternalTypeMaps = new(typeReferenceEqualityComparer); _referencedProxyTypeMaps = new(typeReferenceEqualityComparer); var typeMapResolver = new TypeMapResolver(entryPointAssembly); @@ -70,12 +80,11 @@ public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, Type MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); } } - if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<(CustomAttributeWithOrigin Attr, AssemblyDefinition? TargetAssembly)>? assemblyTargets)) { - foreach (var entry in assemblyTargets) + foreach (var (entry, targetAssembly) in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); - MarkTypeMapAttribute(entry, info); + MarkAssemblyTargetIfReady(typeMapGroup, entry, targetAssembly, callingMethod); } } } @@ -91,12 +100,11 @@ public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeRef MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); } } - if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<(CustomAttributeWithOrigin Attr, AssemblyDefinition? TargetAssembly)>? assemblyTargets)) { - foreach (var entry in assemblyTargets) + foreach (var (entry, targetAssembly) in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); - MarkTypeMapAttribute(entry, info); + MarkAssemblyTargetIfReady(typeMapGroup, entry, targetAssembly, callingMethod); } } } @@ -110,6 +118,44 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) if (entry.TargetType is { } targetType && _context.Resolve(UnwrapToResolvableType(targetType)) is TypeDefinition targetTypeDef) _markStep.MarkRequirementsForInstantiatedTypes(targetTypeDef); + + // When a TypeMap or TypeMapAssociation entry is marked, the origin assembly becomes "alive" for + // type-map purposes. Trigger any pending TypeMapAssemblyTarget attributes that are waiting for + // this assembly to have at least one surviving entry. + if (entry.Attribute.AttributeType.Name is "TypeMapAttribute`1" or "TypeMapAssociationAttribute`1") + TriggerPendingAssemblyTargets(entry.Origin); + } + + void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOrigin entry, AssemblyDefinition? targetAssembly, MethodDefinition? callingMethod) + { + // If the target assembly could not be resolved, the attribute cannot safely be kept: at runtime + // Assembly.Load would throw FileNotFoundException. Drop it silently. + if (targetAssembly is null) + return; + + if (_context.Annotations.IsMarked(targetAssembly)) + { + // Target assembly is already in the output because it has surviving TypeMap entries. + MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod)); + } + else + { + // Target assembly is not yet marked. Defer: mark when (if) the assembly eventually gets a + // surviving TypeMap entry marked. + _pendingAssemblyTargetsByAssembly.AddToList(targetAssembly, (typeMapGroup, entry, callingMethod)); + } + } + + void TriggerPendingAssemblyTargets(AssemblyDefinition markedAssembly) + { + // When a TypeMap/TypeMapAssociation entry is marked, its origin assembly becomes alive. If there + // are any TypeMapAssemblyTarget attributes waiting for this assembly to be alive, mark them now. + // The group visibility check is implicit: entries are only added to this dict after the group is seen. + if (!_pendingAssemblyTargetsByAssembly.Remove(markedAssembly, out List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>? waiting)) + return; + + foreach (var (_, attr, callingMethod) in waiting) + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod)); } public void ProcessType(TypeDefinition definition) @@ -192,21 +238,16 @@ static TypeReference UnwrapToResolvableType(TypeReference type) return type; } - private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOrigin attr) + private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOrigin attr, AssemblyDefinition? resolvedTargetAssembly) { // Validate attribute if (attr.Attribute.ConstructorArguments is not ([{ Value: string }])) return; - // If the type map group has been seen, mark the attribute immediately - if (_referencedExternalTypeMaps.Contains(typeMapGroup) || _referencedProxyTypeMaps.Contains(typeMapGroup)) - { - _markStep.MarkCustomAttribute(attr.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, null), new MessageOrigin(attr.Origin)); - return; - } - - // Otherwise, it's pending until the type map group is seen - _pendingAssemblyTargets.AddToList(typeMapGroup, attr); + // Otherwise, it's pending until the type map group is seen. + // Note: resolvedTargetAssembly may be null if the assembly could not be resolved (e.g., it doesn't + // exist in the input). In that case the attribute will be dropped (not marked) when the group is seen. + _pendingAssemblyTargets.AddToList(typeMapGroup, (attr, resolvedTargetAssembly)); } @@ -297,18 +338,20 @@ public void Resolve(LinkContext context, TypeMapHandler manager) } else if (attr.AttributeType.Name is "TypeMapAssemblyTargetAttribute`1") { - manager.AddAssemblyTarget(typeMapGroup, (attr, assembly)); + AssemblyDefinition? resolvedTargetAssembly = null; if (attr.ConstructorArguments[0].Value is string str) { var nextAssemblyName = AssemblyNameReference.Parse(str); if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) { + resolvedTargetAssembly = nextAssembly; if (seen.Add(nextAssembly)) { toVisit.Enqueue(nextAssembly); } } } + manager.AddAssemblyTarget(typeMapGroup, (attr, assembly), resolvedTargetAssembly); } } } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs new file mode 100644 index 00000000000000..6f7d7659a1d8f9 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs @@ -0,0 +1,17 @@ +// 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. + +// All TypeMap entries in this assembly are conditional (3-argument form). +// The trim target (AllConditionalTrimTarget) is never referenced by the test assembly, +// so ILLink drops the entry, which leaves this assembly with no surviving TypeMap entries. +// The test verifies that the TypeMapAssemblyTarget attribute pointing here is also removed. +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +[assembly: TypeMap("ConditionalEntry", typeof(AllConditionalTarget), typeof(AllConditionalTrimTarget))] + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + public class AllConditionalTarget; + public class AllConditionalTrimTarget; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs new file mode 100644 index 00000000000000..c8c6959e795d5f --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + // Group marker type used by TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed. + // Lives in its own assembly so the test assembly's TypeMapAssemblyTarget + // generic argument does not create a compile-time reference to the conditional.dll dependency. + public class AllConditionalGroupType; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs new file mode 100644 index 00000000000000..9d18f57897e7ea --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs @@ -0,0 +1,36 @@ +// 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.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +// The test assembly references "conditional.dll" only by the assembly name string. +// All TypeMap entries in conditional.dll are conditional on AllConditionalTrimTarget, +// which is never marked. So conditional.dll ends up with no surviving TypeMap entries. +// The fix should cause this TypeMapAssemblyTarget attribute to be removed. +// (No KeptAttributeAttribute here - the test framework verifies the attribute is NOT in the linked output.) +[assembly: TypeMapAssemblyTarget("conditional")] + +namespace Mono.Linker.Tests.Cases.Reflection +{ + // Compile the group-type assembly first so both the test assembly and conditional.dll can reference it. + // Compile conditional.dll second with addAsReference:false so the test assembly has no compile-time + // dependency on it (only the string reference in TypeMapAssemblyTarget). + [SetupCompileBefore("allconditionalgroup.dll", new[] { "Dependencies/TypeMapAllConditionalGroupDep.cs" })] + [SetupCompileBefore("conditional.dll", new[] { "Dependencies/TypeMapAllConditionalEntriesDep.cs" }, + references: new[] { "allconditionalgroup.dll" }, addAsReference: false)] + [SetupLinkerAction("link", "System.Private.CoreLib")] // Needed to apply embedded XML (RemoveAttributeInstances) + [SetupLinkerArgument("--ignore-link-attributes", "false")] + [Kept] + class TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed + { + [Kept] + static void Main() + { + // Use the group so the trimmer processes the TypeMapAssemblyTarget attribute. + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + } + } +} From 2c9bb9f7f528e54717e46a371ba8ca6a1d257653 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:27:37 +0000 Subject: [PATCH 3/9] Address code review: rename param, improve comments in TypeMapHandler.cs and assemblynative.cpp Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com> --- src/coreclr/vm/assemblynative.cpp | 10 ++++++---- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/coreclr/vm/assemblynative.cpp b/src/coreclr/vm/assemblynative.cpp index 36c6d41e07e6e2..52ea7d84fbb637 100644 --- a/src/coreclr/vm/assemblynative.cpp +++ b/src/coreclr/vm/assemblynative.cpp @@ -1628,11 +1628,13 @@ namespace // silently skipped rather than crashing the process with FileNotFoundException. Assembly* pAssembly = spec.LoadAssembly(FILE_LOADED, FALSE); - // If the assembly could not be found, skip it. This can happen when an assembly was - // referenced only through a TypeMapAssemblyTargetAttribute string name and was later - // trimmed away. The trimmer should have removed the attribute, but if it did not (e.g. - // in a non-trimmed scenario or due to a trimmer bug), we treat the missing assembly as + // If the assembly could not be found, skip it and continue processing other targets. + // This can happen when an assembly was referenced only through a + // TypeMapAssemblyTargetAttribute string name and was later trimmed away. + // The trimmer should have removed the attribute, but if it did not (e.g. in a + // non-trimmed scenario or due to a trimmer bug), we treat the missing assembly as // contributing no entries rather than crashing the app. + // Return TRUE to indicate success and continue processing remaining attributes. if (pAssembly == NULL) { return TRUE; diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index cbf2d0164956ea..2bb10e310c43e7 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -128,8 +128,10 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOrigin entry, AssemblyDefinition? targetAssembly, MethodDefinition? callingMethod) { - // If the target assembly could not be resolved, the attribute cannot safely be kept: at runtime - // Assembly.Load would throw FileNotFoundException. Drop it silently. + // If the target assembly could not be resolved (it is not present in the linker input), + // the attribute cannot safely be kept: at runtime Assembly.Load would throw + // FileNotFoundException. Drop it silently; the assembly simply does not participate + // in the type map. if (targetAssembly is null) return; @@ -146,12 +148,12 @@ void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOr } } - void TriggerPendingAssemblyTargets(AssemblyDefinition markedAssembly) + void TriggerPendingAssemblyTargets(AssemblyDefinition newlyMarkedAssembly) { // When a TypeMap/TypeMapAssociation entry is marked, its origin assembly becomes alive. If there // are any TypeMapAssemblyTarget attributes waiting for this assembly to be alive, mark them now. // The group visibility check is implicit: entries are only added to this dict after the group is seen. - if (!_pendingAssemblyTargetsByAssembly.Remove(markedAssembly, out List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>? waiting)) + if (!_pendingAssemblyTargetsByAssembly.Remove(newlyMarkedAssembly, out List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>? waiting)) return; foreach (var (_, attr, callingMethod) in waiting) From 516910c289ce758a2cb7e9e61bb2917faab15b98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:15:08 +0000 Subject: [PATCH 4/9] Revert CoreCLR assemblynative.cpp changes - fix is trimmer-only Co-authored-by: AaronRobinsonMSFT <30635565+AaronRobinsonMSFT@users.noreply.github.com> --- src/coreclr/vm/assemblynative.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/coreclr/vm/assemblynative.cpp b/src/coreclr/vm/assemblynative.cpp index 52ea7d84fbb637..97ee4dfc6dfbab 100644 --- a/src/coreclr/vm/assemblynative.cpp +++ b/src/coreclr/vm/assemblynative.cpp @@ -1624,21 +1624,7 @@ namespace AssemblySpec spec; spec.Init(assemblyNameString); - // Use fThrowOnFileNotFound=FALSE so that a missing assembly (e.g. trimmed away) is - // silently skipped rather than crashing the process with FileNotFoundException. - Assembly* pAssembly = spec.LoadAssembly(FILE_LOADED, FALSE); - - // If the assembly could not be found, skip it and continue processing other targets. - // This can happen when an assembly was referenced only through a - // TypeMapAssemblyTargetAttribute string name and was later trimmed away. - // The trimmer should have removed the attribute, but if it did not (e.g. in a - // non-trimmed scenario or due to a trimmer bug), we treat the missing assembly as - // contributing no entries rather than crashing the app. - // Return TRUE to indicate success and continue processing remaining attributes. - if (pAssembly == NULL) - { - return TRUE; - } + Assembly* pAssembly = spec.LoadAssembly(FILE_LOADED); // Only add the assembly if it is unknown. if (_toProcess.Lookup(pAssembly) == NULL From 1a5f5d0c523f9a05af8daf2ec4fff46fcdedffcf Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:47:21 -0700 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs index 9d18f57897e7ea..20e7e710ea112c 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs @@ -9,8 +9,7 @@ // The test assembly references "conditional.dll" only by the assembly name string. // All TypeMap entries in conditional.dll are conditional on AllConditionalTrimTarget, // which is never marked. So conditional.dll ends up with no surviving TypeMap entries. -// The fix should cause this TypeMapAssemblyTarget attribute to be removed. -// (No KeptAttributeAttribute here - the test framework verifies the attribute is NOT in the linked output.) +// The fix should cause this TypeMapAssemblyTarget attribute to be removed from the linked output. [assembly: TypeMapAssemblyTarget("conditional")] namespace Mono.Linker.Tests.Cases.Reflection @@ -23,6 +22,8 @@ namespace Mono.Linker.Tests.Cases.Reflection references: new[] { "allconditionalgroup.dll" }, addAsReference: false)] [SetupLinkerAction("link", "System.Private.CoreLib")] // Needed to apply embedded XML (RemoveAttributeInstances) [SetupLinkerArgument("--ignore-link-attributes", "false")] + [RemovedAssembly("conditional.dll")] + [RemovedAttributeInAssembly("test", typeof(TypeMapAssemblyTargetAttribute))] [Kept] class TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed { From 27cd6c11a31f207ebecb84177f84f9b3bf1ecd74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:08:03 +0000 Subject: [PATCH 6/9] Fix ordering: call TriggerPendingAssemblyTargets from MarkAssembly in MarkStep Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../src/linker/Linker.Steps/MarkStep.cs | 3 +++ .../src/linker/Linker/TypeMapHandler.cs | 24 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs index 1cccdecc476ae9..611af2c59cdf87 100644 --- a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs @@ -1488,6 +1488,9 @@ public virtual void MarkAssembly(AssemblyDefinition assembly, DependencyInfo rea if (CheckProcessed(assembly)) return; + // Flush any TypeMapAssemblyTarget attributes that were waiting for this assembly to be marked. + _typeMapHandler.TriggerPendingAssemblyTargets(assembly); + var assemblyOrigin = new MessageOrigin(assembly); EmbeddedXmlInfo.ProcessDescriptors(assembly, Context); diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 2bb10e310c43e7..8b2bd8992e0748 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -119,9 +119,11 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) && _context.Resolve(UnwrapToResolvableType(targetType)) is TypeDefinition targetTypeDef) _markStep.MarkRequirementsForInstantiatedTypes(targetTypeDef); - // When a TypeMap or TypeMapAssociation entry is marked, the origin assembly becomes "alive" for - // type-map purposes. Trigger any pending TypeMapAssemblyTarget attributes that are waiting for - // this assembly to have at least one surviving entry. + // MarkAssembly (above) already calls TriggerPendingAssemblyTargets on its first invocation. + // This explicit call handles the rare case where MarkAssembly for entry.Origin had already + // run before any TypeMap entry in that assembly was marked (so nothing was pending at that + // time), and new entries were later deferred while the assembly was still not yet marked. + // In practice it is usually a no-op, but it guarantees correctness regardless of ordering. if (entry.Attribute.AttributeType.Name is "TypeMapAttribute`1" or "TypeMapAssociationAttribute`1") TriggerPendingAssemblyTargets(entry.Origin); } @@ -137,22 +139,24 @@ void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOr if (_context.Annotations.IsMarked(targetAssembly)) { - // Target assembly is already in the output because it has surviving TypeMap entries. + // Target assembly is already marked. Ideally we would only keep the attribute when the + // assembly has at least one surviving TypeMap/TypeMapAssociation entry, but checking that + // here would add complexity for a narrow case. We accept this slight over-approximation. MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod)); } else { - // Target assembly is not yet marked. Defer: mark when (if) the assembly eventually gets a - // surviving TypeMap entry marked. + // Target assembly is not yet marked. Defer: mark when (if) the assembly eventually gets marked. _pendingAssemblyTargetsByAssembly.AddToList(targetAssembly, (typeMapGroup, entry, callingMethod)); } } - void TriggerPendingAssemblyTargets(AssemblyDefinition newlyMarkedAssembly) + // Called from MarkStep.MarkAssembly whenever an assembly is first marked (for any reason), and + // also from MarkTypeMapAttribute when a TypeMap/TypeMapAssociation entry is marked. The two call + // sites together ensure that pending TypeMapAssemblyTarget attributes are flushed regardless of + // the order in which assemblies are visited. + internal void TriggerPendingAssemblyTargets(AssemblyDefinition newlyMarkedAssembly) { - // When a TypeMap/TypeMapAssociation entry is marked, its origin assembly becomes alive. If there - // are any TypeMapAssemblyTarget attributes waiting for this assembly to be alive, mark them now. - // The group visibility check is implicit: entries are only added to this dict after the group is seen. if (!_pendingAssemblyTargetsByAssembly.Remove(newlyMarkedAssembly, out List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>? waiting)) return; From 0d63a4015f5a83231ef59e93277ff29fa2c82e1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:28:37 +0000 Subject: [PATCH 7/9] Remove redundant TriggerPendingAssemblyTargets call from MarkTypeMapAttribute, update comment Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../illink/src/linker/Linker/TypeMapHandler.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 8b2bd8992e0748..013e7708d097f9 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -118,14 +118,6 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) if (entry.TargetType is { } targetType && _context.Resolve(UnwrapToResolvableType(targetType)) is TypeDefinition targetTypeDef) _markStep.MarkRequirementsForInstantiatedTypes(targetTypeDef); - - // MarkAssembly (above) already calls TriggerPendingAssemblyTargets on its first invocation. - // This explicit call handles the rare case where MarkAssembly for entry.Origin had already - // run before any TypeMap entry in that assembly was marked (so nothing was pending at that - // time), and new entries were later deferred while the assembly was still not yet marked. - // In practice it is usually a no-op, but it guarantees correctness regardless of ordering. - if (entry.Attribute.AttributeType.Name is "TypeMapAttribute`1" or "TypeMapAssociationAttribute`1") - TriggerPendingAssemblyTargets(entry.Origin); } void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOrigin entry, AssemblyDefinition? targetAssembly, MethodDefinition? callingMethod) @@ -151,9 +143,8 @@ void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOr } } - // Called from MarkStep.MarkAssembly whenever an assembly is first marked (for any reason), and - // also from MarkTypeMapAttribute when a TypeMap/TypeMapAssociation entry is marked. The two call - // sites together ensure that pending TypeMapAssemblyTarget attributes are flushed regardless of + // Called from MarkStep.MarkAssembly whenever an assembly is first marked (for any reason). + // This ensures that pending TypeMapAssemblyTarget attributes are flushed regardless of // the order in which assemblies are visited. internal void TriggerPendingAssemblyTargets(AssemblyDefinition newlyMarkedAssembly) { From e25f2fb7649121448ec9d1717fadc64525124267 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:28:03 -0700 Subject: [PATCH 8/9] Preserve TypeMap assembly target fast path Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8d5de1a2-a6e8-46fb-84d2-6a971d76419c --- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 013e7708d097f9..7a7c3afd94025a 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -241,6 +241,13 @@ private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOr if (attr.Attribute.ConstructorArguments is not ([{ Value: string }])) return; + // If the type map group has been seen, process the attribute immediately. + if (_referencedExternalTypeMaps.Contains(typeMapGroup) || _referencedProxyTypeMaps.Contains(typeMapGroup)) + { + MarkAssemblyTargetIfReady(typeMapGroup, attr, resolvedTargetAssembly, callingMethod: null); + return; + } + // Otherwise, it's pending until the type map group is seen. // Note: resolvedTargetAssembly may be null if the assembly could not be resolved (e.g., it doesn't // exist in the input). In that case the attribute will be dropped (not marked) when the group is seen. From a5eaec7cf8053dc6d4017f5b0e3f3fa8faf4dbb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:50:04 +0000 Subject: [PATCH 9/9] Add TypeMap test to ILTrim expected failures Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt index 722055538468f5..73f6ca88a01229 100644 --- a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt +++ b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt @@ -353,6 +353,7 @@ Reflection.RunClassConstructor Reflection.TypeHierarchyLibraryModeSuppressions Reflection.TypeHierarchyReflectionWarnings Reflection.TypeMap +Reflection.TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed Reflection.TypeUsedViaReflection Reflection.UnsafeAccessor RequiresCapability.BasicRequires