Skip to content

Commit ff646e0

Browse files
jtschustersbomer
andauthored
Re-add static interface trimming with more testing (#2791)
Enables more precise removal of static abstract interface methods and adds more tests than previously. Co-authored-by: Sven Boemer <[email protected]>
1 parent 93de720 commit ff646e0

File tree

11 files changed

+1837
-39
lines changed

11 files changed

+1837
-39
lines changed

docs/removal-behavior.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class Program
4242

4343
### Method call on a constrained type parameter
4444

45-
On a call to a static abstract interface method that is accessed through a constrained type parameter, the interface method is rooted, as well as every implementation method on every type.
45+
On a call to a static abstract interface method that is accessed through a constrained type parameter, the interface method is kept, as is every implementation method on every kept type.
4646

4747
Example:
4848

src/linker/Linker.Steps/MarkStep.cs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ protected LinkContext Context {
6060

6161
protected Queue<(MethodDefinition, DependencyInfo, MessageOrigin)> _methods;
6262
protected List<(MethodDefinition, MarkScopeStack.Scope)> _virtual_methods;
63+
protected List<(MethodDefinition, MarkScopeStack.Scope)> _static_interface_methods;
6364
protected Queue<AttributeProviderPair> _assemblyLevelAttributes;
6465
readonly List<AttributeProviderPair> _ivt_attributes;
6566
protected Queue<(AttributeProviderPair, DependencyInfo, MarkScopeStack.Scope)> _lateMarkedAttributes;
@@ -224,6 +225,7 @@ public MarkStep ()
224225
{
225226
_methods = new Queue<(MethodDefinition, DependencyInfo, MessageOrigin)> ();
226227
_virtual_methods = new List<(MethodDefinition, MarkScopeStack.Scope)> ();
228+
_static_interface_methods = new List<(MethodDefinition, MarkScopeStack.Scope)> ();
227229
_assemblyLevelAttributes = new Queue<AttributeProviderPair> ();
228230
_ivt_attributes = new List<AttributeProviderPair> ();
229231
_lateMarkedAttributes = new Queue<(AttributeProviderPair, DependencyInfo, MarkScopeStack.Scope)> ();
@@ -476,6 +478,7 @@ bool ProcessPrimaryQueue ()
476478
while (!QueueIsEmpty ()) {
477479
ProcessQueue ();
478480
ProcessVirtualMethods ();
481+
ProcessStaticInterfaceMethods ();
479482
ProcessMarkedTypesWithInterfaces ();
480483
ProcessDynamicCastableImplementationInterfaces ();
481484
ProcessPendingBodies ();
@@ -576,6 +579,30 @@ void ProcessVirtualMethods ()
576579
}
577580
}
578581

582+
/// <summary>
583+
/// Handles marking implementations of static interface methods and the interface implementations of types that implement a static interface method.
584+
/// </summary>
585+
void ProcessStaticInterfaceMethods ()
586+
{
587+
foreach ((MethodDefinition method, MarkScopeStack.Scope scope) in _static_interface_methods) {
588+
using (ScopeStack.PushScope (scope)) {
589+
var overrides = Annotations.GetOverrides (method);
590+
if (overrides != null) {
591+
foreach (OverrideInformation @override in overrides) {
592+
ProcessOverride (@override);
593+
// We need to mark the interface implementation for static interface methods
594+
// Explicit interface method implementations already mark the interface implementation in ProcessMethod
595+
MarkExplicitInterfaceImplementation (@override.Override, @override.Base);
596+
}
597+
}
598+
}
599+
}
600+
}
601+
602+
/// <summary>
603+
/// Does extra handling of marked types that have interfaces when it's necessary to know what types are marked or instantiated.
604+
/// Right now it only marks the "implements interface" annotations and removes override annotations for static interface methods.
605+
/// </summary>
579606
void ProcessMarkedTypesWithInterfaces ()
580607
{
581608
// We may mark an interface type later on. Which means we need to reprocess any time with one or more interface implementations that have not been marked
@@ -693,6 +720,9 @@ void ProcessVirtualMethod (MethodDefinition method)
693720
}
694721
}
695722

723+
/// <summary>
724+
/// Handles marking overriding methods if the type with the overriding method is instantiated or if the base method is a static abstract interface method
725+
/// </summary>
696726
void ProcessOverride (OverrideInformation overrideInformation)
697727
{
698728
var method = overrideInformation.Override;
@@ -708,12 +738,12 @@ void ProcessOverride (OverrideInformation overrideInformation)
708738

709739
var isInstantiated = Annotations.IsInstantiated (method.DeclaringType);
710740

711-
// We don't need to mark overrides until it is possible that the type could be instantiated
741+
// We don't need to mark overrides until it is possible that the type could be instantiated or the method is a static interface method
712742
// Note : The base type is interface check should be removed once we have base type sweeping
713743
if (IsInterfaceOverrideThatDoesNotNeedMarked (overrideInformation, isInstantiated))
714744
return;
715745

716-
// Interface static veitual methods will be abstract and will also by pass this check to get marked
746+
// Interface static virtual methods will be abstract and will also bypass this check to get marked
717747
if (!isInstantiated && !@base.IsAbstract && Context.IsOptimizationEnabled (CodeOptimizations.OverrideRemoval, method))
718748
return;
719749

@@ -736,8 +766,7 @@ bool IsInterfaceOverrideThatDoesNotNeedMarked (OverrideInformation overrideInfor
736766
if (!overrideInformation.IsOverrideOfInterfaceMember || isInstantiated)
737767
return false;
738768

739-
// This is a static interface method and these checks should all be true
740-
if (overrideInformation.Override.IsStatic && overrideInformation.Base.IsStatic && overrideInformation.Base.IsAbstract && !overrideInformation.Override.IsVirtual)
769+
if (overrideInformation.IsStaticInterfaceMethodPair)
741770
return false;
742771

743772
if (overrideInformation.MatchingInterfaceImplementation != null)
@@ -3054,17 +3083,28 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo
30543083
}
30553084
}
30563085

3086+
// Mark overridden methods and interface implementations except for static interface methods
3087+
// This will not mark implicit interface methods because they do not have a MethodImpl and aren't in the .Overrides
30573088
if (method.HasOverrides) {
3058-
foreach (MethodReference ov in method.Overrides) {
3059-
MarkMethod (ov, new DependencyInfo (DependencyKind.MethodImplOverride, method), ScopeStack.CurrentScope.Origin);
3060-
MarkExplicitInterfaceImplementation (method, ov);
3089+
foreach (MethodReference @base in method.Overrides) {
3090+
// Method implementing a static interface method will have an override to it - note nonstatic methods usually don't unless they're explicit.
3091+
// Calling the implementation method directly has no impact on the interface, and as such it should not mark the interface or its method.
3092+
// Only if the interface method is referenced, then all the methods which implemented must be kept, but not the other way round.
3093+
if (Context.Resolve (@base) is MethodDefinition baseDefinition
3094+
&& new OverrideInformation.OverridePair (baseDefinition, method).IsStaticInterfaceMethodPair ())
3095+
continue;
3096+
MarkMethod (@base, new DependencyInfo (DependencyKind.MethodImplOverride, method), ScopeStack.CurrentScope.Origin);
3097+
MarkExplicitInterfaceImplementation (method, @base);
30613098
}
30623099
}
30633100

30643101
MarkMethodSpecialCustomAttributes (method);
30653102
if (method.IsVirtual)
30663103
_virtual_methods.Add ((method, ScopeStack.CurrentScope));
30673104

3105+
if (method.IsStatic && method.IsAbstract && method.DeclaringType.IsInterface)
3106+
_static_interface_methods.Add ((method, ScopeStack.CurrentScope));
3107+
30683108
MarkNewCodeDependencies (method);
30693109

30703110
MarkBaseMethods (method);
@@ -3147,9 +3187,9 @@ protected virtual IEnumerable<MethodDefinition> GetRequiredMethodsForInstantiate
31473187
}
31483188
}
31493189

3150-
void MarkExplicitInterfaceImplementation (MethodDefinition method, MethodReference ov)
3190+
void MarkExplicitInterfaceImplementation (MethodDefinition method, MethodReference overriddenMethod)
31513191
{
3152-
if (Context.Resolve (ov) is not MethodDefinition resolvedOverride)
3192+
if (Context.Resolve (overriddenMethod) is not MethodDefinition resolvedOverride)
31533193
return;
31543194

31553195
if (resolvedOverride.DeclaringType.IsInterface) {
@@ -3421,7 +3461,7 @@ void MarkInterfacesNeededByBodyStack (MethodBody body)
34213461
{
34223462
// If a type could be on the stack in the body and an interface it implements could be on the stack on the body
34233463
// then we need to mark that interface implementation. When this occurs it is not safe to remove the interface implementation from the type
3424-
// even if the type is never instantiated
3464+
// even if the type is never instantiated. (ex. `Type1 x = null; IFoo y = (IFoo)x;`)
34253465
var implementations = new InterfacesOnStackScanner (Context).GetReferencedInterfaces (body);
34263466
if (implementations == null)
34273467
return;

src/linker/Linker.Steps/SweepStep.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)
453453

454454
SweepCustomAttributes (method.MethodReturnType);
455455

456+
SweepOverrides (method);
457+
456458
if (!method.HasParameters)
457459
continue;
458460

@@ -467,6 +469,38 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)
467469
}
468470
}
469471

472+
void SweepOverrides (MethodDefinition method)
473+
{
474+
for (int i = 0; i < method.Overrides.Count;) {
475+
// We can't rely on the context resolution cache anymore, since it may remember methods which are already removed
476+
// So call the direct Resolve here and avoid the cache.
477+
// We want to remove a method from the list of Overrides if:
478+
// Resolve() is null
479+
// This can happen for a couple of reasons, but it indicates the method isn't in the final assembly.
480+
// Resolve also may return a removed value if method.Overrides[i] is a MethodDefinition. In this case, Resolve short circuits and returns `this`.
481+
// OR
482+
// ov.DeclaringType is null
483+
// ov.DeclaringType may be null if Resolve short circuited and returned a removed method. In this case, we want to remove the override.
484+
// OR
485+
// ov is in a `link` scope and is unmarked
486+
// ShouldRemove returns true if the method is unmarked, but we also We need to make sure the override is in a link scope.
487+
// Only things in a link scope are marked, so ShouldRemove is only valid for items in a `link` scope.
488+
if (method.Overrides[i].Resolve () is not MethodDefinition ov || ov.DeclaringType is null || (IsLinkScope (ov.DeclaringType.Scope) && ShouldRemove (ov)))
489+
method.Overrides.RemoveAt (i);
490+
else
491+
i++;
492+
}
493+
}
494+
495+
/// <summary>
496+
/// Returns true if the assembly of the <paramref name="scope"></paramref> is set to link
497+
/// </summary>
498+
private bool IsLinkScope (IMetadataScope scope)
499+
{
500+
AssemblyDefinition? assembly = Context.Resolve (scope);
501+
return assembly != null && Annotations.GetAction (assembly) == AssemblyAction.Link;
502+
}
503+
470504
void SweepDebugInfo (Collection<MethodDefinition> methods)
471505
{
472506
List<ScopeDebugInformation>? sweptScopes = null;

src/linker/Linker/Annotations.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ public bool IsPublic (IMetadataTokenProvider provider)
436436
return public_api.Contains (provider);
437437
}
438438

439+
/// <summary>
440+
/// Returns an IEnumerable of the methods that override this method. Note this is different than <see cref="MethodDefinition.Overrides"/>, which returns the MethodImpl's
441+
/// </summary>
439442
public IEnumerable<OverrideInformation>? GetOverrides (MethodDefinition method)
440443
{
441444
return TypeMapInfo.GetOverrides (method);

src/linker/Linker/OverrideInformation.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@ namespace Mono.Linker
1010
public class OverrideInformation
1111
{
1212
readonly ITryResolveMetadata resolver;
13+
readonly OverridePair _pair;
1314

1415
public OverrideInformation (MethodDefinition @base, MethodDefinition @override, ITryResolveMetadata resolver, InterfaceImplementation? matchingInterfaceImplementation = null)
1516
{
16-
Base = @base;
17-
Override = @override;
17+
_pair = new OverridePair (@base, @override);
1818
MatchingInterfaceImplementation = matchingInterfaceImplementation;
1919
this.resolver = resolver;
2020
}
2121

22-
public MethodDefinition Base { get; }
23-
public MethodDefinition Override { get; }
22+
public readonly record struct OverridePair (MethodDefinition Base, MethodDefinition Override)
23+
{
24+
public bool IsStaticInterfaceMethodPair () => Base.DeclaringType.IsInterface && Base.IsStatic && Override.IsStatic;
25+
}
26+
27+
public MethodDefinition Base { get => _pair.Base; }
28+
public MethodDefinition Override { get => _pair.Override; }
2429
public InterfaceImplementation? MatchingInterfaceImplementation { get; }
2530

2631
public bool IsOverrideOfInterfaceMember {
@@ -43,5 +48,7 @@ public TypeDefinition? InterfaceType {
4348
return Base.DeclaringType;
4449
}
4550
}
51+
52+
public bool IsStaticInterfaceMethodPair => _pair.IsStaticInterfaceMethodPair ();
4653
}
4754
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
namespace ILLink.RoslynAnalyzer.Tests.Inheritance.Interfaces
8+
{
9+
public sealed partial class StaticInterfaceMethodsTests : LinkerTestBase
10+
{
11+
protected override string TestSuiteName => "Inheritance.Interfaces.StaticInterfaceMethods";
12+
13+
[Fact]
14+
public Task StaticAbstractInterfaceMethods ()
15+
{
16+
return RunTest (nameof (StaticAbstractInterfaceMethods));
17+
}
18+
19+
[Fact]
20+
public Task StaticAbstractInterfaceMethodsLibrary ()
21+
{
22+
return RunTest (nameof (StaticAbstractInterfaceMethodsLibrary));
23+
}
24+
}
25+
}

test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/InterfaceVariants.cs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,53 +71,51 @@ internal class ImplementsUnusedStaticInterface
7171
// The interface methods themselves are not used, but the implementation of these methods is
7272
internal interface IStaticInterfaceMethodUnused
7373
{
74-
// Can be removed with Static Interface trimming optimization
75-
[Kept]
7674
static abstract void InterfaceUsedMethodNot ();
7775
}
7876

79-
// Can be removed with Static Interface Trimming
80-
[Kept]
8177
internal interface IStaticInterfaceUnused
8278
{
83-
// Can be removed with Static Interface Trimming
84-
[Kept]
8579
static abstract void InterfaceAndMethodNoUsed ();
8680
}
8781

8882
[Kept]
89-
[KeptInterface (typeof (IStaticInterfaceUnused))]
90-
[KeptInterface (typeof (IStaticInterfaceMethodUnused))]
9183
internal class InterfaceMethodUsedThroughImplementation : IStaticInterfaceMethodUnused, IStaticInterfaceUnused
9284
{
9385
[Kept]
86+
[RemovedOverride (typeof (IStaticInterfaceMethodUnused))]
9487
public static void InterfaceUsedMethodNot () { }
9588

9689
[Kept]
90+
[RemovedOverride (typeof (IStaticInterfaceUnused))]
9791
public static void InterfaceAndMethodNoUsed () { }
9892
}
9993

10094
[Kept]
101-
[KeptInterface (typeof (IStaticInterfaceMethodUnused))]
102-
[KeptInterface (typeof (IStaticInterfaceUnused))]
10395
internal class InterfaceMethodUnused : IStaticInterfaceMethodUnused, IStaticInterfaceUnused
10496
{
105-
[Kept]
10697
public static void InterfaceUsedMethodNot () { }
10798

108-
[Kept]
10999
public static void InterfaceAndMethodNoUsed () { }
110100
}
111101

102+
[Kept]
103+
// This method keeps InterfaceMethodUnused without making it 'relevant to variant casting' like
104+
// doing a typeof or type argument would do. If the type is relevant to variant casting,
105+
// we will keep all interface implementations for interfaces that are kept
106+
internal static void KeepInterfaceMethodUnused (InterfaceMethodUnused x) { }
107+
112108
[Kept]
113109
public static void Test ()
114110
{
115111
InterfaceMethodUsedThroughImplementation.InterfaceUsedMethodNot ();
116112
InterfaceMethodUsedThroughImplementation.InterfaceAndMethodNoUsed ();
117113

118-
Type t;
119-
t = typeof (IStaticInterfaceMethodUnused);
120-
t = typeof (InterfaceMethodUnused);
114+
// The interface has to be kept this way, because if both the type and the interface may
115+
// appear on the stack then they would be marked as relevant to variant casting and the
116+
// interface implementation would be kept.
117+
Type t = typeof (IStaticInterfaceMethodUnused);
118+
KeepInterfaceMethodUnused (null);
121119
}
122120
}
123121

0 commit comments

Comments
 (0)