-
Notifications
You must be signed in to change notification settings - Fork 21
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
Bridge methods prevent forward-compatible library evolution #11804
Comments
Just to ease understanding: I think you are swapping which method is a bridge and which one is the real method. The real method is the one with the refined type. The bridge is the one with the exact same signature as in the parent class, and which at run-time delegates to the real method. It is you are arguing for removing the real method, not the bridge. Not generating the bridge and keeping only the real method would not have the correct semantics at all. |
Yes, the real method needs to get the inherited signature, therefore no bridge method will be generated. After reading your comment I see how the terminology is confusing. |
Yeah, the signature of the bridge method with the body of the real method. The annotation suppresses the covariant return type in the method signature, thus alleviating the need for a bridging method. Having this fixes the issue like in the example, where the return type is really being refined elsewhere via a type parameter. But if the return type is being refined in a more independent way (e.g. a method in Seq returns |
No change is visible from Scala, only erased types are affected. The original motivation (in Java) appears to be keeping the raw types better aligned with the generic types. This was a fair point at the time (covariant return types were introduced in 5.0 together with generics; interoperability with pre-5.0 Java was a big issue) but doesn't look very useful anymore in a 2019 Scala context. |
Sounds good! Do you have a list of PRs that we could merge / re-open with this fixed? |
@szeiger The erased type being affected matters, but I realise my example would behave differently: |
@dwijnand It doesn't matter if you refine the return type explicitly or through tighter type bounds. In both cases the compiler knows the overriding pair and generates an erased signature and potentially a bridge method accordingly. This is the part that needs to be changed (i.e. erase to the overridden method's erasure instead). |
@lrytz I can't recall any PR that we couldn't do at all. There is a comment in I remember several other cases where we used these hacks but they were in 2.12. 2.13 is still too young to have accumulated a large armount of them. |
@szeiger Here's the test case I'm thinking: class Foo {
def create: Foo = new Foo
}
class Bar extends Foo {
@nobridge override def create: Bar = new Bar
} If we make Bar's |
The erased signatures don't matter. The compiler will automatically insert the cast. It does that all the time when you use type members and type variables. If the return type was an abstract type that you refined in |
I spent several days looking into Erasure to try and find a good place to fit this in but came up with nothing. Someone with more intimate knowledge of the compiler may have a better chance. This just touches too much surface area and erasure appears to be a particularly tricky part. The way I envisioned it is to erase the method types as if they were the parent types. Then everything else should fall into place automatically. Bridges are skipped if the erased types match, etc. But this is not how erasure works. You first get a Computing different erased types in the erasure phase alone doesn't really help. They would be inconsistent with direct uses of erasure at other points. I think it would solve the composition problem though because an InfoTransform is always based on the complete type of a symbol. Could this be the way forward? I think we'd have to first get rid of all direct uses of erasure outside of the erasure phase (in refchecks, mixin and jvm) in order to make this work. Another option is to move the implementation into the bridge (or rather what would otherwise be the bridge) and remove the original method when generating bridges. But then all call sites need to be rewritten, plus any method that calls erasure before the erasure phase would operate on the wrong method (the one that is eventually removed). This seems a bit too sketchy. |
@szeiger I did a small experiment - did you try this approach? It seems to work for the example, also in separate compilation (if only https://github.com/scala/scala/compare/2.13.x...lrytz:inheritSignature?expand=1 What doesn't work yet is that casts are needed within the method implementation when accessing a parameter that has a weaker type inherited from the parent, or when returning a primitive. See comments in the test. |
@lrytz is this blocker for 2.12.13? And/or 2.13.4? |
No I don't think so |
If this is fixed, we can re-attempt to land the follow (I'll edit the comment over time): |
Closing, as there doesn't seem to be a solution to this (see scala/scala#9141) |
While we are going to abandon forward binary compatibility in the long run, we still need to preserve it for the 3.0 transition (probably until we switch to a 3.x library in 3.1 and tooling has been adapted to the new compatibility model).
Currently bridge methods are a big problem for forward compatibility. Example:
Enabling the override in
B
creates two separate methods:Any caller that sees an instance of
B
asB
will call the "real" method with the refined signature (due to the tighter upper bound ofT
compared to the original version), callers that only see anA
call the bridge method (which overrides the inherited signature) instead. This is not forward compatible because the "real" method did not exist in the old version.There is no good reason for the bridge method to exist in such a case, except to potentially avoid some casts due to the tighter bounds. Note that without the explicit override the class still overrides
foo
in order to delegate to the interface's default implementation but this method uses the original bounds so there is no bridge method.This case frequently comes up in the collections library where the
C
andCC[_]
type parameters are refined through the hierarchy. There is no workaround via casts or@unsafSomething
annotations so we have to resort to hacking the override into the original implementation in some superclass usingisInstanceOf
. Depending on access restrictions of other parts of the class this is not always possible. Where it is possible the extra type checks make all uses slower (even when not related to the class that should be changed) and create more work down the line to switch to the proper design the next time we can break forward compatibility.We should add an annotation
@nobridge
than can be added to an overridden method to prevent the generation of a bridge method. There are cases where a bridge method is really needed (when access restrictions get relaxed) but in most cases it could be suppressed. The only work required when breaking forward compatibility would be the removal of these annotations.The text was updated successfully, but these errors were encountered: