Skip to content

NativeAOT: make external TypeMap trimming array-aware with MdArray element fallback#127135

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/modify-get-effective-trim-target-type
Draft

NativeAOT: make external TypeMap trimming array-aware with MdArray element fallback#127135
Copilot wants to merge 5 commits intomainfrom
copilot/modify-get-effective-trim-target-type

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 20, 2026

Description

NativeAOT was treating SomeType[] trim targets as SomeType, which could keep TypeMap entries when only the element type was reachable. This updates array handling to condition on array type existence (or template-based runtime constructibility), matching NativeAOT's higher precision goals.

  • Compiler change: preserve arrays as trim targets

    • GetEffectiveTrimTargetType now strips only non-array parameterized wrappers (pointer/byref), and keeps ArrayType intact.
    • This makes TypeMap dependency conditions use NecessaryTypeSymbol(arrayType) instead of collapsing to element type.
  • Compiler change: array template fallback for non-template-eligible arrays

    • Existing canonical-template condition continues to cover template-eligible array forms (e.g., SZ arrays with supported element types).
    • Added fallback for arrays whose canonical form is not template-eligible (e.g., multidimensional arrays, pointer-element arrays): if the de-parameterized element canonical type is template-eligible, condition NecessaryTypeSymbol(arrayType) on that element template.
  • Compiler change: MdArray element type reachability fallback

    • For multidimensional (non-SzArray) arrays, the runtime can construct them dynamically from just the element type's MethodTable (e.g., via shared generic code that creates T[,]). Added a conditional dependency: if NecessaryTypeSymbol(elementType) is marked, mark NecessaryTypeSymbol(mdArrayType) as reachable. This ensures TypeMap entries for MdArray trim targets are included when the element type is reachable, since the runtime type loader can create MdArrays on demand.
  • Smoke test update: TestInteropMapArrayTrimming

    • Added TypeMap entry H (TrimTarget8[] -> Target8) with TrimTarget8/Target8 types. Makes TrimTarget8 reachable without constructing TrimTarget8[], and asserts H is absent (validates SzArray precision).
    • Entry I (TrimTarget9[,] -> Target9) added by reviewer: validates that MdArray trim targets are present when the element type is reachable and the array is constructed at runtime via shared generic code (MakeSharedGeneric<TrimTarget9>()new T[1,1]).
private static TypeDesc GetEffectiveTrimTargetType(TypeDesc trimmingTargetType)
{
    while (trimmingTargetType is ParameterizedType parameterized && !trimmingTargetType.IsArray)
        trimmingTargetType = parameterized.ParameterType;
    return trimmingTargetType;
}
Original prompt

Background

PR #126380 (merged) fixed a crash when array types are used as TypeMap trim targets. The fix in ExternalTypeMapNode.cs strips all parameterized types (arrays, pointers, byrefs) to the element type via GetEffectiveTrimTargetType, then conditions on NecessaryTypeSymbol(elementType). This matches ILLink's behavior but is suboptimal for Native AOT.

Problem

The current implementation is imprecise: if SomeType exists in the program but SomeType[] does not, a TypeMap entry with SomeType[] as the trim target will still be included. Native AOT has the capability to track whether the array type itself exists (via MethodTable / NecessaryTypeSymbol for the array, or via type loader templates for arrays with generic element types).

Requested Change

In src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs:

  1. Modify GetEffectiveTrimTargetType: Stop stripping array types (ArrayType). Only strip non-array parameterized types (pointers, byrefs). Array types should be kept as-is so we condition on the array type itself.

  2. Update GetConditionalStaticDependencies: For array types, use NecessaryTypeSymbol(arrayType) as the condition. Additionally, handle template-based runtime construction:

    • If the array type's canonical form is eligible for a template (SzArray with non-pointer element types like TrimTarget1<__Canon>[]), add a conditional dependency: if the template exists → mark NecessaryTypeSymbol(arrayType). This is already handled by the existing template check code since GenericTypesTemplateMap.IsEligibleToHaveATemplate already handles arrays via IsArrayTypeEligibleForTemplate.
    • For array types whose canonical form is NOT template-eligible (MdArrays like Foo[,], pointer arrays like Foo*[]), also check if the element type's canonical form has a template. If so, the runtime can construct the element type dynamically and then create the array from it. Strip parameterized wrappers from the element type (e.g., for Foo*[], get to Foo), check if the canonical form is template-eligible, and add the conditional dependency.
  3. GetMarkedEntries should also work correctly since it calls GetEffectiveTrimTargetType - same changes apply.

Key Insight on Template Eligibility

From GenericTypesTemplateMap.cs:

  • IsEligibleToHaveATemplate returns true for types that HasInstantiation OR IsArrayTypeEligibleForTemplate
  • IsArrayTypeEligibleForTemplate returns true only for SzArray (IsSzArray) whose element type is not a pointer or function pointer
  • So TrimTarget1<__Canon>[] (SzArray) → eligible
  • TrimTarget2<__Canon>[,] (MdArray) → NOT eligible (needs element type template fallback)
  • TrimTarget3<__Canon>*[] (SzArray of pointers) → NOT eligible (needs element type template fallback)

Test

Add a test to src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs specifically targeting this optimization. The test should add an entry (e.g., "H") where:

  • The element type IS reachable (e.g., via an is TrimTarget8 check or new TrimTarget8())
  • But the array type TrimTarget8[] is never constructed
  • With the optimization, entry H should be absent from the map (because NecessaryTypeSymbol(TrimTarget8[]) is never marked, even though NecessaryTypeSymbol(TrimTarget8) is marked)

Add this to the existing TestInteropMapArrayTrimming class. Add:

  • A new TypeMap assembly attribute for entry "H" with a new TrimTarget8 struct and Target8 class
  • In the Run() method, use TrimTarget8 directly (e.g., if (GetUnknown() is TrimTarget8) ...) to make the element type reachable without making the array type reachable
  • Assert that entry H is absent: if (map.TryGetValue("H", out _)) throw new Exception("Unexpected entry H");

This test validates that the optimization works: conditioning on the array type (not element type) correctly excludes entries where only the element type is reachable.

Expected Results After Change

For the existing test entries:

  • A (TrimTarget1<Atom>[]): SzArray, canon TrimTarget1<__Canon>[] has template → present
  • B (TrimTarget2<Atom>[,]): MdArray, no array template, but element TrimTarget2<__Canon> has template → present
  • C (TrimTarget3<Atom>*[]): SzArray of pointers, no array template, but element TrimTarget3<__Canon> has template → present
  • D (TrimTarget4<Atom>[]): SzArray, template for TrimTarget4<__Canon>[] would exist only if referenced in compiled code. MakeGenerics skips TrimTarget4 → absent
  • E (TrimTarget5[]): Non-generic, NecessaryTypeSymbol(TrimTarget5[]) never marked → absent
  • F (TrimTarget6[]): Non-generic, NecessaryTypeSymbol(TrimTarget6[]) marked via new TrimTarget6[1] in MakeGenerics → present
  • G (TrimTarget7[]): Non-generic, NecessaryTypeSymbol(TrimTarget7[]) marked via is TrimTarget7[] cast → present
  • H (n...

This pull request was created from Copilot chat.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b403556c-8176-43d3-a272-e0f2dd338c46

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 20, 2026 06:31
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b403556c-8176-43d3-a272-e0f2dd338c46

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 21, 2026 02:20
@MichalStrehovsky
Copy link
Copy Markdown
Member

@copilot in the latest commit I added a new test. it fails with:

System.Exception: Expected entry I
   at DeadCodeElimination.TestInteropMapArrayTrimming.Run()
   at DeadCodeElimination.Run()
   at Program.<<Main>$>g__RunTest|0_0(Func`1, String)

Should the test succeed? If so, fix the bug in the compiler

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves NativeAOT’s external TypeMap trimming precision for array trim targets by conditioning map entries on the array type’s reachability (and, for certain arrays, on template-based runtime constructibility), and extends the NativeAOT trimming smoke test coverage.

Changes:

  • Update ExternalTypeMapNode.GetEffectiveTrimTargetType to preserve array types (only unwrap pointer/byref), so TypeMap conditions track array existence rather than collapsing to the element type.
  • Add a template-based fallback for arrays that don’t have array templates (e.g., MD arrays / pointer-element arrays) by conditioning array reachability on a template-loadable canonical element type.
  • Extend the TestInteropMapArrayTrimming smoke test with new TypeMap entries and additional reflection-based coverage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs Keeps arrays as trim targets and adds conditional dependencies to account for runtime-constructible arrays via templates.
src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs Adds new TypeMap entries and runtime reachability patterns to validate array-aware TypeMap trimming behavior.

Comment on lines 23 to +27
[assembly: TypeMap<DeadCodeElimination.TestInteropMapArrayTrimming>("E", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target5), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget5[]))]
[assembly: TypeMap<DeadCodeElimination.TestInteropMapArrayTrimming>("F", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target6), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget6[]))]
[assembly: TypeMap<DeadCodeElimination.TestInteropMapArrayTrimming>("G", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target7), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget7[]))]
[assembly: TypeMap<DeadCodeElimination.TestInteropMapArrayTrimming>("H", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target8), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget8[]))]
[assembly: TypeMap<DeadCodeElimination.TestInteropMapArrayTrimming>("I", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target9), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget9[,]))]
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions adding only the new "H" regression case in TestInteropMapArrayTrimming, but this diff also adds a new TypeMap entry "I" (TrimTarget9[,] -> Target9) and additional reflection-based helpers to exercise it. Please either update the PR description to cover the new "I" scenario (and why it's needed), or drop the extra test additions if they’re not intended for this change.

Copilot uses AI. Check for mistakes.
Comment on lines 1451 to +1452
// A, B, C, F, G: trim target element type is reachable — entries must be present
// I: trim target can be constructed at runtime using type loader
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment says entry "I" is expected because the trim target can be constructed at runtime using the type loader, but the trim target is TrimTarget9[,] where TrimTarget9 is a non-generic DefType. In ExternalTypeMapNode, the new template-based fallback only fires when the (de-parameterized) element type has a canonical form eligible for templates (i.e., canonElementType != effectiveElementType and IsEligibleToHaveATemplate(canonElementType)), which won’t be true for TrimTarget9. Consider clarifying what runtime-constructibility mechanism this test is validating, or adjust the trim target to one whose element type is actually template-loadable so the test aligns with the intended coverage.

Suggested change
// A, B, C, F, G: trim target element type is reachable — entries must be present
// I: trim target can be constructed at runtime using type loader
// A, B, C, F, G: trim target element type is reachable — entries must be present.
// I: expected due to the shared-generic instantiation exercised above via TestSharedGeneric,
// not because TrimTarget9[,] can use the template-based type-loader fallback for canonical element types.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Test comment

Generated by Code Review for issue #127135 ·

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/67701700-796d-45b7-9aa8-f035408d84d4

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

@copilot in the latest commit I added a new test. it fails with:

...

Fixed in 2c07962. The issue was that for MdArray types (non-SzArray), the runtime can construct them dynamically from just the element type's MethodTable (e.g., via shared generic code that creates T[,]). Added a conditional dependency: when the element type is reachable, mark the MdArray as reachable too. This doesn't affect SzArrays (case H still correctly absent).

Copilot AI changed the title NativeAOT: make external TypeMap trimming array-aware and add H regression case NativeAOT: make external TypeMap trimming array-aware with MdArray element fallback Apr 21, 2026
@MichalStrehovsky
Copy link
Copy Markdown
Member

MichalStrehovsky commented Apr 21, 2026

The issue was that for MdArray types (non-SzArray), the runtime can construct them dynamically from just the element type's MethodTable (e.g., via shared generic code that creates T[,]). Added a conditional dependency: when the element type is reachable, mark the MdArray as reachable too. This doesn't affect SzArrays (case H still correctly absent).

@copilot Does this match the logic in IsEligibleToHaveATemplate or are there more gaps?

@MichalStrehovsky
Copy link
Copy Markdown
Member

@copilot react to comment #127135 (comment)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

The issue was that for MdArray types (non-SzArray), the runtime can construct them dynamically from just the element type's MethodTable (e.g., via shared generic code that creates T[,]). Added a con...

No, the MdArray fallback doesn't match IsEligibleToHaveATemplate — it uses a different mechanism. Looking at TypeBuilderState.TemplateType (lines 59-68), MdArrays use typeof(object[,]) as a hardcoded template that's always available, bypassing GenericTypesTemplateMap entirely. So the runtime only needs the element type's MethodTable to construct any MdArray, which is why I condition on element type reachability.

As for gaps: pointer/function-pointer element SzArrays have a similar property — they use the hardcoded typeof(char*[]) template (lines 72-80). My code handles generic pointer-element arrays via the element template fallback (lines 62-70), but doesn't add an element-reachability fallback for non-generic pointer-element SzArrays (e.g., SomeNonGenericType*[]). That's an analogous gap, though likely not a practical concern since pointer arrays created through shared generics are uncommon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants