Fix illink TypeMap proxy retention. Fixes #127004.#127005
Fix illink TypeMap proxy retention. Fixes #127004.#127005rolfbjarne wants to merge 2 commits intodotnet:mainfrom
Conversation
Keep matching TypeMapAssociation entries when an external TypeMap entry preserves the same source type. Fixes dotnet#127004. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @agocke, @dotnet/illink |
There was a problem hiding this comment.
Pull request overview
Fixes an ILLink trimming bug where TypeMapAssociationAttribute<T> entries can be dropped when the same type is also preserved/instantiated via a TypeMapAttribute<T> entry (issue #127004).
Changes:
- Adds a new trimming test scenario intended to validate retention of both
TypeMapandTypeMapAssociationentries for the same type. - Updates
TypeMapHandler.MarkTypeMapAttributeto additionally mark associated proxyTypeMapAssociationentries when aTypeMapAttributemarks a type as instantiated.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs |
Adds a new test scenario asserting both TypeMap and TypeMapAssociation attributes are kept for a shared type. |
src/tools/illink/src/linker/Linker/TypeMapHandler.cs |
Adds logic to mark proxy association attributes when a TypeMapAttribute instantiates the same type, to avoid missing proxy processing due to early exit in MarkStep. |
| void MarkAssociatedProxyTypeMapAttributes(TypeDefinition sourceType, TypeReference typeMapGroup) | ||
| { | ||
| if (_unmarkedProxyTypeMapEntries.TryGetValue(sourceType, out Dictionary<TypeReference, List<CustomAttributeWithOrigin>>? entriesByGroup) && | ||
| entriesByGroup.Remove(typeMapGroup, out List<CustomAttributeWithOrigin>? unmarkedAttributes)) | ||
| { | ||
| foreach (var attr in unmarkedAttributes) | ||
| MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, sourceType)); | ||
|
|
||
| if (entriesByGroup.Count == 0) | ||
| _unmarkedProxyTypeMapEntries.Remove(sourceType); | ||
| } | ||
|
|
||
| if (_pendingProxyTypeMapEntries.TryGetValue(typeMapGroup, out List<CustomAttributeWithOrigin>? pendingAttributes)) |
There was a problem hiding this comment.
MarkAssociatedProxyTypeMapAttributes directly calls MarkTypeMapAttribute on TypeMapAssociation entries as soon as a TypeMapAttribute marks the same type instantiated. This bypasses the existing universe separation logic (proxy entries are supposed to be kept only when the proxy type-map group is referenced via GetOrCreateProxyTypeMapping), and can cause TypeMapAssociation attributes to be preserved even when the proxy map group is never seen. Consider instead triggering the same logic as MarkStep would (e.g., invoke ProcessInstantiated/RecordTargetTypeSeen for the instantiated type) so entries are moved to the pending/marked sets respecting _referencedProxyTypeMaps, rather than being unconditionally marked here.
| void MarkAssociatedProxyTypeMapAttributes(TypeDefinition sourceType, TypeReference typeMapGroup) | |
| { | |
| if (_unmarkedProxyTypeMapEntries.TryGetValue(sourceType, out Dictionary<TypeReference, List<CustomAttributeWithOrigin>>? entriesByGroup) && | |
| entriesByGroup.Remove(typeMapGroup, out List<CustomAttributeWithOrigin>? unmarkedAttributes)) | |
| { | |
| foreach (var attr in unmarkedAttributes) | |
| MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, sourceType)); | |
| if (entriesByGroup.Count == 0) | |
| _unmarkedProxyTypeMapEntries.Remove(sourceType); | |
| } | |
| if (_pendingProxyTypeMapEntries.TryGetValue(typeMapGroup, out List<CustomAttributeWithOrigin>? pendingAttributes)) | |
| void MarkOrQueueProxyTypeMapAttributes(List<CustomAttributeWithOrigin> attributes, TypeReference typeMapGroup, DependencyInfo info) | |
| { | |
| if (_referencedProxyTypeMaps.Contains(typeMapGroup)) | |
| { | |
| foreach (CustomAttributeWithOrigin attr in attributes) | |
| MarkTypeMapAttribute(attr, info); | |
| return; | |
| } | |
| if (!_pendingProxyTypeMapEntries.TryGetValue(typeMapGroup, out List<CustomAttributeWithOrigin>? pendingAttributes)) | |
| { | |
| pendingAttributes = new List<CustomAttributeWithOrigin>(); | |
| _pendingProxyTypeMapEntries.Add(typeMapGroup, pendingAttributes); | |
| } | |
| pendingAttributes.AddRange(attributes); | |
| } | |
| void MarkAssociatedProxyTypeMapAttributes(TypeDefinition sourceType, TypeReference typeMapGroup) | |
| { | |
| if (_unmarkedProxyTypeMapEntries.TryGetValue(sourceType, out Dictionary<TypeReference, List<CustomAttributeWithOrigin>>? entriesByGroup) && | |
| entriesByGroup.Remove(typeMapGroup, out List<CustomAttributeWithOrigin>? unmarkedAttributes)) | |
| { | |
| MarkOrQueueProxyTypeMapAttributes(unmarkedAttributes, typeMapGroup, new DependencyInfo(DependencyKind.TypeMapEntry, sourceType)); | |
| if (entriesByGroup.Count == 0) | |
| _unmarkedProxyTypeMapEntries.Remove(sourceType); | |
| } | |
| if (_referencedProxyTypeMaps.Contains(typeMapGroup) && | |
| _pendingProxyTypeMapEntries.TryGetValue(typeMapGroup, out List<CustomAttributeWithOrigin>? pendingAttributes)) |
| [assembly: TypeMap<UsedExternalTypeMap>("BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] // Kept | ||
| [assembly: TypeMapAssociation<UsedExternalTypeMap>(typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] // Kept | ||
| [assembly: KeptAttributeAttribute(typeof(TypeMapAttribute<UsedExternalTypeMap>), "BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] | ||
| [assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute<UsedExternalTypeMap>), typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] |
There was a problem hiding this comment.
This new scenario asserts that TypeMapAssociation is kept, but the test body never calls TypeMapping.GetOrCreateProxyTypeMapping(). That conflicts with the earlier test intent/comments that proxy-universe attributes should be trimmed when only the external map is used for a universe. To make this a valid regression for #127004, consider using a universe that actually exercises both external and proxy type maps (e.g., UsedTypeMap), or explicitly reference the proxy type map group under test and adjust the other "proxy attrs removed" expectations accordingly.
| [assembly: TypeMap<UsedExternalTypeMap>("BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] // Kept | |
| [assembly: TypeMapAssociation<UsedExternalTypeMap>(typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] // Kept | |
| [assembly: KeptAttributeAttribute(typeof(TypeMapAttribute<UsedExternalTypeMap>), "BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] | |
| [assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute<UsedExternalTypeMap>), typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] | |
| [assembly: TypeMap<UsedTypeMap>("BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] // Kept | |
| [assembly: TypeMapAssociation<UsedTypeMap>(typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] // Kept | |
| [assembly: KeptAttributeAttribute(typeof(TypeMapAttribute<UsedTypeMap>), "BothInExternalAndProxy", typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxy))] | |
| [assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute<UsedTypeMap>), typeof(BothInExternalAndProxy), typeof(BothInExternalAndProxyTarget))] |
| _ = TypeMapping.GetOrCreateExternalTypeMapping<string>(); | ||
| _ = TypeMapping.GetOrCreateProxyTypeMapping<string>(); | ||
|
|
||
| // Use BothInExternalAndProxy in a way that preserves any corresponding typemap entries. |
There was a problem hiding this comment.
Instantiating BothInExternalAndProxy here roots the type, but it still doesn't ensure the proxy type-map group for the added TypeMapAssociation attribute is referenced. If the intent is to cover the bug where the proxy association is missed because the type was already marked instantiated by an external TypeMap entry, the test should also ensure the relevant proxy type-map universe is "seen" (via GetOrCreateProxyTypeMapping for the same group) so the association is expected to be kept.
| // Use BothInExternalAndProxy in a way that preserves any corresponding typemap entries. | |
| // Use BothInExternalAndProxy in a way that preserves any corresponding typemap entries. | |
| // Explicitly reference the proxy type-map universe for the same group as the external entry, | |
| // so the test validates that the proxy association is kept even when the type was already | |
| // marked instantiated by the external type map. | |
| _ = TypeMapping.GetOrCreateProxyTypeMapping<UsedExternalTypeMap>(); |
|
I think a cleaner fix (if it works) would be to just make |
Use MarkRequirementsForInstantiatedTypes instead of directly calling Annotations.MarkInstantiated in TypeMapHandler.MarkTypeMapAttribute. This ensures ProcessInstantiated is called for the target type, which properly triggers proxy TypeMapAssociation entry processing through the existing RecordTargetTypeSeen logic, removing the need for the custom MarkAssociatedProxyTypeMapAttributes method. Also fix the test to use UsedTypeMap (which has both external and proxy references) instead of UsedExternalTypeMap (which only has external). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep matching TypeMapAssociation entries when an external TypeMap entry preserves the same source type.
Note: copilot wrote the fix, I have no idea if it's correct or not.
Fixes #127004.