-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add bridges for overridden methods in lambda indy call #6087
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
Conversation
* non-empty to empty following `erasure`; it seems that this is always | ||
* initialized pre-erasure, so post-erasure this method is no longer | ||
* equivalent to `allOverriddenSymbols.nonEmpty` like it claims. | ||
*/ | ||
lazy val isOverridingSymbol = ( | ||
canMatchInheritedSymbols | ||
&& owner.ancestors.exists(base => overriddenSymbol(base) != NoSymbol) |
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.
Another drive-by warning: this cache can serve up outdated values in multi-Run compilation if the ancestors change! We should store either the Run
or maybe the full Period
alongside this boolean and recompute if needed, as is done for other caches in *Symbol
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'll put that in a separate commit once I deal with the ComputeBridges
suggestions.
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 wound up just caching allOverriddenSymbols
...
It appears that isOverridingSymbol
gets called (after typer
) mainly in constructors
and explicitouter
, and probably at most once per symbol per each of those phases, so I don't feel that bad about moving the cache to allOverriddenSymbols
.
@@ -1349,23 +1349,36 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { | |||
val constrainedType = MethodBType(lambdaParams.map(p => typeToBType(p.tpe)), typeToBType(lambdaTarget.tpe.resultType)).toASMType | |||
val samMethodType = methodBTypeFromSymbol(sam).toASMType | |||
val markers = if (addScalaSerializableMarker) classBTypeFromSymbol(definitions.SerializableClass).toASMType :: Nil else Nil | |||
visitInvokeDynamicInsnLMF(bc.jmethod, sam.name.toString, invokedType, samMethodType, implMethodHandle, constrainedType, isSerializable, markers) | |||
val overriddenMethods = enteringErasure(sam.allOverriddenSymbols).map(o => methodBTypeFromSymbol(o).toASMType) |
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.
It would be ideal to unify the computation of the required bridges with the normal bridge logic in Erasure
. We would need to change it to recognize that Function
nodes are classes-in-waiting, and as such should have ComputeBridges
applied. The listing of required bridges could be attached to the function symbol (probably inside the existing attachment, SAMFunction
.)
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 implementation of ComputeBridges
as a SymbolPair
-s walk has an efficiency argument over finding overriden members decl
-wise. For SAM functions that won't make a difference, though, as we've only decl.
So the argument for unifying the logic would be more about keeping responsibilities clear, which might help curtail bugs down the track.
LGTM |
Why would it be binary incompatible to add a default method to an interface that already inherits a signature for that method (although abstract)? |
So I checked with Scala.js, and the test case also causes a linking error in Scala.js:
It seems that, with the fix in this PR, we'll have to duplicate this logic with |
@sjrd thanks for the heads up. I guessed without checking, since this showed up with the 2.12 LMF-based lambda encoding, that it was JVM-specific. Now I guess not. The signature of |
For all LMF-based lambdas, Scala.js has to replicate whatever the JVM back-end + LMF itself do in the Scala.js back-end. It's mostly around |
@sjrd my bad w/r/t "binary compatibility", I think; it shouldn't matter with the 2.12 trait encoding. However, there is another consideration here, which is that if the traits are defined in library A, and project B wants to use the lambda syntax, with this change the project B only needs to bump to a newer version of the compiler, whereas with the Java-style default method fix B would need to wait for A to recompile with the newer version and release. Perhaps this could be revisited for 2.13, which would simplify things for SJS (it wouldn't be a backend change anymore) but it seems awkward for 2.12. If that sounds better I can slap a [nomerge] on this and make that change against 2.13.x. |
Ah, good point. IMO your plan sounds good 👍 |
4c9aae9
to
4a03e6d
Compare
/* Arguably I should do `fun.setSymbol(samCls)` rather than leaning | ||
* on an attachment, but doing that confounds lambdalift's free var | ||
* analysis in a way which does not seem to be trivially reparable. */ | ||
fun.updateAttachment(SAMFunction(samTp, sam, samCls)) |
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.
@retronym it was plan "B", I'm afraid...
FWIW: with a Before:
After:
(cumulative stats, of course) |
overridingPeriod = currentPeriod | ||
overridingCache = result | ||
result | ||
} |
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'm reluctant to introduce this cache. This method is called on each decl in each class symbol (source or classpath based) during specialization, and these caches will hang around until the end of the compiler. It appears as though the caches would also consume O(N^2) space wrt the length of the override chain (the N-th deepest symbol would have a cache of N-1 elements).
I forget the exact reason that the cache in isOverridingSymbol
got in the way of this bugfix, could you refresh my memory? My changes to isOverridingSymbol
in #6197 will conflict here.
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.
Yeah, I saw those changes and they're probably better. I did this because I read the "we should" in your comment to mean "I should, and now". I'm just going to drop that commit from this PR.
@@ -1349,23 +1351,37 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { | |||
val constrainedType = MethodBType(lambdaParams.map(p => typeToBType(p.tpe)), typeToBType(lambdaTarget.tpe.resultType)).toASMType | |||
val samMethodType = methodBTypeFromSymbol(sam).toASMType | |||
val markers = if (addScalaSerializableMarker) classBTypeFromSymbol(definitions.SerializableClass).toASMType :: Nil else Nil | |||
visitInvokeDynamicInsnLMF(bc.jmethod, sam.name.toString, invokedType, samMethodType, implMethodHandle, constrainedType, isSerializable, markers) | |||
val overriddenMethods = | |||
bridges.map(b => methodBTypeFromSymbol(b).toASMType) |
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'm pretty sure that methodBTypeFromSymbol
is sufficient to ensure that the backend include types within to InnerClasses
, @lrytz can you confirm this.
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'll come up with a test to be sure.
case bridge: MethodSymbol if bridge hasFlag BRIDGE => bridge | ||
}.toList | ||
} | ||
} |
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.
userSamCls.findMembers(0L, BRIDGE)
might also work here (we know the only base class is Object
without bridges).
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.
Seems to work, and you're right that our base types are only the SAM trait and Object
. I'll change it.
* | ||
* @since 2.12.0-M4 | ||
*/ | ||
case class SAMFunction(samTp: Type, sam: Symbol) extends PlainAttachment | ||
case class SAMFunction(samTp: Type, sam: Symbol, samCls: Symbol) extends PlainAttachment |
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.
Perhaps we should call this syntheticSamClass
here and in vals the reference it (e.g. userSamCls
below) to make it really clear that it isn't the functional interface.
* to go somewhere. Erasure knows to compute bridges for these classes | ||
* just as if they were real templates extending the SAM type. */ | ||
val samCls = fun.symbol.owner.newClassWithInfo( | ||
name = TypeName(s"${samTp.typeSymbol.name}$$${sam.nameString}$$samImpl"), |
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 a name for this? I'd probably just use tpnme.ANON_FUN_NAME
.
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 don't. That's better.
assert(phase == currentRun.erasurePhase, phase) | ||
assert(lambdaClass.isClass, lambdaClass) | ||
val eb = new EnterBridges(unit, lambdaClass) | ||
eb.computeAndEnter(includeDeferred = 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.
Could you add a comment on the reason for includeDeferred = true
? It isn't clear to me.
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.
Ugh. I recall needing this, but I didn't comment it and now I can't remember. I'll poke at it until I find out why.
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. I think it is that at one point I was cloning the SAM's MethodSymbol
and not changing its flags, so it would show up deferred here. Good catch.
If a SAM trait's abstract method overrides a method in a supertrait while changing the return type, the generated invokedynamic instruction needs to pass the types of the overridden methods to `LambdaMetaFactory` so that bridge methods can be added to the generated lambda. SAM `Function` trees now have a synthetic `ClassSymbol`, into which we enter an overriding concrete method symbol that represents the runtime LMF-generated implementation of that function. `erasure` then computes bridge methods for that class, which are added to the `LMF.metafactory` call in `jvm`. Java does this instead by generating a default method in the subinterface overriding the superinterface's method. Theoretically, we could generate the bridges in the same way, but that has the downside that the project with the interfaces would need recompiled, which might not be the project that creates the lambdas. Generating bridges the LMF way means that only the classes affected by this bug need to be recompiled to get the fix. Honestly I'm surprised that this hasn't come up already. Fixes scala/bug#10512.
4a03e6d
to
9812c7d
Compare
/rebuild |
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 end result LGTM!
@adriaanm Could you please review as well, in particular, the changes around inferSamType
?
@sjrd I believe this version should be relatively straight forward for you to support in ScalaJS. Out of curiousity, would your life be easier if, under ScalaJS mode, we just desugared user defined SAMs to anonymous classes after typechecking? Or do you have a runtime component analagous to LambdaMetafactory
?
@retronym thanks :D Sorry for biting off a bit much. I'm willing to jump into the scala.js code this weekend to update it for this, assuming that this PR is in vaguely final shape. |
It would, but only ever so slightly. It would allow us to remove What is more annoying for us is actually the early desugaring of SAMs for
We might eventually have a link-time component for that, but I'm not sure. For 0.6.x and even until 1.0.0, that is very unlikely to happen.
Cool :) Make sure to work on the 0.6.x branch if you do. We regularly forward merge 0.6.x into master. |
@sjrd Just mentioning you to highlight that this is (finally) merged and you can plan the downstream changes. |
Thanks. I was keeping an eye on this one ;) |
This is current as of scala/scala@09ebc826968b.
FTR, here is the Scala.js PR: scala-js/scala-js#3260 |
Adapt scalameta custom analyzer to breaking changes in scala/scala#6087.
I haven't fully gotten to the bottom of this, but is this missing a check that the bridges are indeed for the SAM method (i.e. find member should also pass in |
Looks like it wasn't as 2.13 contains code from this PR, was this explored at all? Also what happens if I write a lambda in Java that calls a Scala trait with such an overridden method, does Java emits code that crashes at runtime? Asking since this is affecting Dotty too: scala/scala3#10068 |
I don't think so...
It seems
The indy-lambda for |
Interesting, so it seems that no matter what we do we still need to be able to generate indy-lambdas that ask for bridges in the backend. I assume javac does that for backward-compatibility with pre-java8 interfaces? |
Thinking about this more, it seems that we cannot assume that a sub-interface coming from Java implements the necessary bridge(s) and so we always need to do the altMetafactory dance: the problem is that under separate compilation we can easily check if the subinterface contains bridges, but under joint compilation of java/scala sources we don't have this information, and we cannot assume that the bridge is always there since it's going to depend on the version of javac (and presumably isn't something mandated by the spec in any version although I haven't checked), to keep the compilation output stable we cannot do something different under joint or separate compilation, therefore we have to be pessimistic and never assume that the bridge is present. |
Partially based on scala/scala#6087 Fixes scala#10068 Fixes scala#11676
Partially based on scala/scala#6087 Fixes scala#10068 Fixes scala#11676
If a SAM trait's abstract method overrides a method in a supertrait while changing the return type, the generated invokedynamic instruction needs to pass the types of the overridden methods to
LambdaMetaFactory
so that bridge methods can be added to the generated lambda.Java does this differently: it generates a default method in the subinterface overriding the superinterface method. Theoretically we could also generate the default bridges, but that is a binary-incompatible change, so I think it's safer to just add the bridges in the invokedynamic.
Honestly I'm surprised that this hasn't come up already.
Fixes scala/bug#10512.