Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDep
context.NativeLayout.TemplateTypeLayout(canonTrimmingType),
"External type map trim target that could be loaded at runtime");
}
else if (effectiveTrimTargetType is ArrayType arrayType)
{
// Some arrays don't have array templates (e.g. multidimensional arrays, arrays of pointers).
// If the element type is template-loadable, the runtime can still construct the array type.
TypeDesc effectiveElementType = GetEffectiveTrimTargetType(arrayType.ElementType);
TypeDesc canonElementType = effectiveElementType.ConvertToCanonForm(CanonicalFormKind.Specific);
if (canonElementType != effectiveElementType && GenericTypesTemplateMap.IsEligibleToHaveATemplate(canonElementType))
{
yield return new CombinedDependencyListEntry(
context.NecessaryTypeSymbol(effectiveTrimTargetType),
context.NativeLayout.TemplateTypeLayout(canonElementType),
"External type map array trim target with template-loadable element type");
}
}
}
}
}
Expand Down Expand Up @@ -100,12 +114,12 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer
}
}

// Strip parameterized type wrappers (arrays, pointers, byrefs) to get the effective
// type for trimming purposes. If the trim target is Foo[], the TypeMap entry should be
// included when Foo is reachable, matching ILLink's TypeMapHandler stripping behavior.
// Strip non-array parameterized wrappers (pointers, byrefs) to get the effective
// trimming target type. Arrays are preserved so trim dependencies can be conditioned
// on array existence rather than just element type reachability.
private static TypeDesc GetEffectiveTrimTargetType(TypeDesc trimmingTargetType)
{
while (trimmingTargetType is ParameterizedType parameterized)
while (trimmingTargetType is ParameterizedType parameterized && !trimmingTargetType.IsArray)
trimmingTargetType = parameterized.ParameterType;
return trimmingTargetType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
[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[,]))]
Comment on lines 23 to +27
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.

class DeadCodeElimination
{
Expand Down Expand Up @@ -1387,13 +1389,17 @@ public struct TrimTarget4<T>;
public struct TrimTarget5;
public struct TrimTarget6;
public struct TrimTarget7;
public struct TrimTarget8;
public class TrimTarget9;
public class Target1;
public class Target2;
public class Target3;
public class Target4;
public class Target5;
public class Target6;
public class Target7;
public class Target8;
public class Target9;
public class Atom;

public static unsafe object[] MakeGenerics<T>()
Expand All @@ -1414,18 +1420,36 @@ public static unsafe object[] MakeGenerics<T>()
[MethodImpl(MethodImplOptions.NoInlining)]
static object GetUnknown() => null;

[MethodImpl(MethodImplOptions.NoInlining)]
public static object MakeSharedGeneric<T>()
=> new T[1,1];

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool TestSharedGeneric<T>()
=> MakeSharedGeneric<T>() is T[,];

[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTrimTarget9() => typeof(TrimTarget9);

public static void Run()
{
if (GetUnknown() is TrimTarget7[])
{
Console.WriteLine("Unexpected");
}
if (GetUnknown() is TrimTarget8)
{
Console.WriteLine("Unexpected");
}

typeof(TestInteropMapArrayTrimming).GetMethod(nameof(MakeGenerics)).MakeGenericMethod([GetAtom()]).Invoke(null, []);

typeof(TestInteropMapArrayTrimming).GetMethod(nameof(TestSharedGeneric)).MakeGenericMethod([GetTrimTarget9()]).Invoke(null, []);

var map = TypeMapping.GetOrCreateExternalTypeMapping<TestInteropMapArrayTrimming>();

// 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
Comment on lines 1451 to +1452
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.
if (!map.TryGetValue("A", out Type typeA) || typeA.Name != nameof(Target1))
throw new Exception("Expected entry A");
ThrowIfUsableMethodTable(typeA);
Expand All @@ -1446,11 +1470,17 @@ public static void Run()
throw new Exception("Expected entry G");
ThrowIfUsableMethodTable(typeG);

// D, E: element type is unreachable — entries must be absent
if (!map.TryGetValue("I", out Type typeI) || typeI.Name != nameof(Target9))
throw new Exception("Expected entry I");
ThrowIfUsableMethodTable(typeI);

// D, E: trim target element type is unreachable. H: element is reachable, but TrimTarget8[] is unreachable.
if (map.TryGetValue("D", out _))
throw new Exception("Unexpected entry D");
if (map.TryGetValue("E", out _))
throw new Exception("Unexpected entry E");
if (map.TryGetValue("H", out _))
throw new Exception("Unexpected entry H");
}
}

Expand Down
Loading