-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Separate interface method trimming logic and update for static interface methods #2868
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change looks good to me, but I am hoping we can get tests in the same PR. Happy to look at tests together to see if we can get them to compile.
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -2321,7 +2321,7 @@ bool IsMethodNeededByTypeDueToPreservedScope (MethodDefinition method) | |||
// If the type is marked, we need to keep overrides of abstract members defined in assemblies | |||
// that are copied. However, if the base method is virtual, then we don't need to keep the override | |||
// until the type could be instantiated | |||
if (!@base.IsAbstract) | |||
if (!(@base.IsAbstract || (@base.IsStatic && @base.IsVirtual))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not commenting on this fix because I don't have enough context anymore, but the comment 7 lines above:
// Just because the type is marked does not mean we need interface methods.
// if the type is never instantiated, interfaces will be removed - but only if the optimization is enabled
indicates that either the comment is incorrect, or the logic in the foreach loop is incorrect for static methods - instantiating a type has no effect on whether the static methods are usable. We should probably not be in the foreach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment is incorrect -- "instantiated" here probably means substituted into a constrained generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Michal on this one - the comment and code don't match my expectation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vitek-karas and I discussed this and came to an understanding of what this method is meant to do. It has been updated with a matrix of when to mark a method or leave it unmarked. This method also assumes that the type is not instantiated.
Just noting that Alternatively, you could pull in the same toolset compiler that dotnet/runtime is using. |
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -2321,7 +2321,7 @@ bool IsMethodNeededByTypeDueToPreservedScope (MethodDefinition method) | |||
// If the type is marked, we need to keep overrides of abstract members defined in assemblies | |||
// that are copied. However, if the base method is virtual, then we don't need to keep the override | |||
// until the type could be instantiated | |||
if (!@base.IsAbstract) | |||
if (!(@base.IsAbstract || (@base.IsStatic && @base.IsVirtual))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could the base method not be static, given that the check above on line 2308 returns false if the current method isn't static?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this comment - virtual instance method will pass all of the checks above and get here. This if here is:
- Move onto potential marking for abstract or static virtual methods
- Continue/Skip for instance virtual methods (as per the comments above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you're right, I misread, this is IsStatic && IsVirtual
, above is IsStatic || IsVirtual
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -2321,7 +2321,7 @@ bool IsMethodNeededByTypeDueToPreservedScope (MethodDefinition method) | |||
// If the type is marked, we need to keep overrides of abstract members defined in assemblies | |||
// that are copied. However, if the base method is virtual, then we don't need to keep the override | |||
// until the type could be instantiated | |||
if (!@base.IsAbstract) | |||
if (!(@base.IsAbstract || (@base.IsStatic && @base.IsVirtual))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment is incorrect -- "instantiated" here probably means substituted into a constrained generic.
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -2321,7 +2321,7 @@ bool IsMethodNeededByTypeDueToPreservedScope (MethodDefinition method) | |||
// If the type is marked, we need to keep overrides of abstract members defined in assemblies | |||
// that are copied. However, if the base method is virtual, then we don't need to keep the override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is now incorrect. I believe the rules for when we keep interface implementations for abstract or virtual members are identical. Is there a case where we might not need to keep an implementation for a virtual member when we would keep it for an abstract one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it applies here but linker must keep overrides of abstract base members if the type is kept - otherwise it's invalid IL (can't have a type which is not abstract and doesn't implement all abstract members of its base type). So non-instantiated types may keep some of their virtual members due to this (although I think linker will generate a throw body for such methods).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, these are static so I don't think the instantiated rule applies -- something like "substituted for a type argument" instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we wait until we see the type as substituted for a type argument? What about MakeGenericType/MakeGenericMethod at runtime?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should treat reflected-over types as possibly substituted for type arguments. I think the IsRelevantToVariantCasting
check covers this and the case where it's accessed via reflection.
[Fact] | ||
public Task StaticVirtualInterfaceMethodsLibrary () | ||
{ | ||
return RunTest (allowMissingWarnings: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably shouldn't allow missing warnings. Move this into the proper test suite?
$"Method {linked} overrides method {overriddenMethod}, but {linked.DeclaringType} does not implement interface {overriddenMethod.DeclaringType}"); | ||
} else { | ||
TypeReference baseType = linked.DeclaringType; | ||
TypeReference overriddenType = overriddenMethod.DeclaringType; | ||
while (baseType is not null) { | ||
if (baseType.Equals (overriddenType)) | ||
if (baseType.FullName == overriddenType.FullName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When could this be different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly don't remember what issue caused me to need to change this, but I think it was related to generics.
src/linker/Linker.Steps/MarkStep.cs
Outdated
if (!@base.DeclaringType.IsInterface && !@base.IsStatic && @base.IsVirtual && !@base.IsAbstract) | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the @base.IsVirtual? I would think it's basically implied (since it's a base method on a class).
src/linker/Linker.Steps/MarkStep.cs
Outdated
// A virtual instance method with a default impl on the base doesn't need to be kept | ||
if (!@base.DeclaringType.IsInterface && !@base.IsStatic && @base.IsVirtual && !@base.IsAbstract) | ||
continue; | ||
// base/iface | inst/stat | virt/abst | iface has static | UnusedIFaceOpt | needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Please separate the various cases with a newline.
src/linker/Linker.Steps/MarkStep.cs
Outdated
// that are copied. However, if the base method is virtual, then we don't need to keep the override | ||
// until the type could be instantiated | ||
if (!@base.IsAbstract) | ||
// ---------------------------------------------------------------------------------- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would preface this with comment which states the base
is always virtual/abstract (since it's a "base" method for an override). I know it's kind of obvious, but I was struggling with it reading the table below.
src/linker/Linker.Steps/MarkStep.cs
Outdated
// iface | instance | abstract | yes | yes | yes | ||
// iface | instance | * | no | yes | no | ||
// iface | static | * | always yes | yes | yes | ||
// iface | * | * | * | no | yes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The *
values are a bit confusing. I guess it means "both", but it's hard to tell. Since each column is effectively a boolean mean just use both
, or either
or something like that.
src/linker/Linker.Steps/MarkStep.cs
Outdated
// iface | * | * | * | no | yes | ||
|
||
|
||
// base/iface | inst/stat | virt/abst | iface has static | UnusedIFaceOpt | needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// base/iface | inst/stat | virt/abst | iface has static | UnusedIFaceOpt | needed | |
// base/iface | inst/stat | virt/abst | iface w/ static | UnusedIFaceOpt | needed |
The specific cases should match the big table exactly, otherwise it's confusing. Also below...
src/linker/Linker.Steps/MarkStep.cs
Outdated
// Uninstantiated types will not need iface instance methods unless they are abstract | ||
// Abstract iface methods must be implemented for valid IL, but not virtuals |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't sound right.
- Uninstantiated types may not even implement the interface
- Typically interface methods are abstract, but if we remove the interface implementation the implementing methods are not needed (since they won't be overrides).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If no test failed on this then we're missing a test... and should add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Short example:
// Not kept
interface IFace // In preserved scope
{
void Method(); // This is abstract effectively
}
[Kept]
class MyType : IFace
{
// Not kept
void Method() { }
}
void Test()
{
typeof(MyType); // The only use of MyType
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out, the comment is very wrong and doesn't properly communicate what the code is meant to do.
src/linker/Linker.Steps/MarkStep.cs
Outdated
// Instance methods of interfaces without static methods don't need marking | ||
// It is still very possible that the interface could be removed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unnecessarily conservative. For example:
interface IFace // In preserved scope
{
void Method(); // Abstract instance
static void IfNotNull(IFace f) { if (f != null) f.Method(); }
}
class MyType : IFace
{
void Method() { }
}
void Test()
{
typeof(MyType); // The only use of the type
}
I think in this case it's OK to remove the implementation of IFace
on MyType
and thus remove MyType.Method
.
I think the interesting case is if an interface has a static virtual/abstract method, then we can't really remove implementation of such interface (in theory we could, but we would have to start tracking generic instantiations and constraints, which currently we don't).
Might be worth adding a helper like CanRemoveInterfaceImplementationForUninstantiatedType(TypeDefinition iface)
which would return true for interfaces for which we could consider removing their implementation (if the type is not instantiated). It would return false if the interface has static virtual/abstract method I think.
The above example with non-virtual static on an interface: I think we'll need to figure out if it's OK to remove that (I could be wrong above). Basically what happens if I do this:
interface IFace
{
static void Hello () { Console.WriteLine("Hello"); }
}
class MyType : IFace
{
}
class HelloThere<T> where T : IFace
{
public static void Greet()
{
T.Hello();
}
}
public static void Test()
{
HelloThere<MyType>.Greet();
}
It seems this doesn't compile, so if this is prohibited then the above holds true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I just found out that if I change it like this:
interface IFace
{
static void Hello () { Console.WriteLine("Hello"); }
}
class MyType : IFace
{
}
class HelloThere<T> where T : IFace
{
public static void Greet()
{
// Removed the call here
}
}
public static void Test()
{
HelloThere<MyType>.Greet();
}
It now compiles, but if I remove IFace implementation from MyType it doesn't compile anymore since MyType now doesn't fulfill the generic constraint. I wonder if we have issue in linker around this at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played with this some more... linker is clever - if it sees interface in a generic constraint then such interface implementation is always preserved on all types (regardless of instantiation).
It's called "relevant to variant casting" (honestly no idea why, but that's what it does).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think my above statement holds true - presence of a simple non-virtual static method on an interface doesn't mean we can't remove the interface implementation from a type.
Given the above about "relevant to variant casting" - we will never remove static interface implementations (since we would see them in generic constraints). Which fits what we discussed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a way we may not need any checks for the static interface cases - due to the fact that linker will keep implementions of ifaces which are used for generic instantiation (and that is the only way static interfaces can be used really - other than reflection). We could keep the code here - as it doesn't hurt, but I would honestly expect it to work even without it (the iface implementation should be kept and since the iface is fully preserved I would expect that all implementation methods on the class are also kept - since they need to be).
Co-authored-by: Sven Boemer <sbomer@gmail.com>
...ce.Interfaces/StaticInterfaceMethods/StaticVirtualInterfaceMethodsInPreservedScopeLibrary.cs
Outdated
Show resolved
Hide resolved
…nker into markStaticVirtuals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking really good, I think this is almost ready!
Co-authored-by: Sven Boemer <sbomer@gmail.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
Co-authored-by: Sven Boemer <sbomer@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good to me, I just think the doc needs to be updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks a lot!
…ace methods (dotnet/linker#2868) Fixes dotnet/linker#2865 Also addresses marking of all static interface methods encompassing the changes from dotnet/linker#2859, and updates the way that all interface methods are marked. Whether or not we mark an interface method due to its base method is now separated from marking other virtual methods and the marking is postponed to ProcessMarkedTypesWithInterface. In ProcessMarkedTypesWithInterfaces, interface implementations are marked, and methods that implement a marked/implemented interface are marked. Tests for static interface methods have also been updated. Co-authored-by: Sven Boemer <sbomer@gmail.com> Commit migrated from dotnet/linker@118bdca
Fixes #2865
The linker expected all static interface methods to be abstract. This fix makes sure static virtual interface methods are also marked.Also updates the linker to consider all unresolved assemblies to be in a Preserve/Ignore scope.I haven't been able to get static virtual interface methods to compile in tests yet, but will add tests once I can.A test has been added to replicate the behavior that caused the issue, as well as the Override checking for tests. More static virtual interface tests will be added in #2859Apologies for the massive diff, most of it is removing tests written in IL that are covered in C# tests now.
This PR now addresses marking of all static interface methods encompassing the changes from #2859, and also updates the way that all interface methods are marked. Whether or not we mark an interface method due to it's base method is now separated from marking other virtual methods and the marking is postponed to ProcessMarkedTypesWithInterface. In ProcessMarkedTypesWithInterfaces, interface implementations are marked, and methods that implement a marked/implemented interface are marked.
In addition, the assembly root mode for each assembly is tracked through LinkContext.AssemblyRootModes to allow the linker to make sure all interface implementations are marked and the methods that implement the interface are kept in library mode.
OverrideInformation was also modified to contain the logic for finding the corresponding interface implementation for a base/override pair.
The VarianceBasic.il test was decompiled to C#.