-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Fix #9970: Reconcile how Scala 3 and 2 decide whether a trait has $init$. #10530
Conversation
Well, I have no idea how to test this in a remotely automated way. See #9970 (comment) What I did to locally test the original report was:
Note in particular that there is no call to I welcome any suggestion how to add a test for this, but in the meantime I will mark this as ready for review. |
Maybe we can add tests in |
No, that wouldn't test what the issue was about. The problem in that specific case was not the bytecode, but the Tasty signatures interpreted by the Tasty reader in Scala 2. The bytecode emitted by Scala 3 hasn't changed a bit in this specific case. |
75d19ef
to
c2f35b8
Compare
I added a TASTy Inspector test, as suggested by Nicolas yesterday. |
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.
For my review I compiled the two examples that were tricky from #9970 with this commit, I then took the latest scala 2.13.x commit, changed the tasty version to 26.0, and compiled and ran the following apps, in-turn, consuming ioapp:
// for the IOApp example with no `run`
object Foo extends IOApp.Simple {}
object Main extends App {
println(Foo)
}
// for the more complex example that defines `run`
object Main extends IOApp.Simple {
def run = println("Foo")
}
there were no crashes. I think the assumption that entering $init$
causes it to be called is correct
…s $init$. Scala 2 uses a very simple mechanism to detect traits that have or don't have a `$init$` method: * At parsing time, a `$init$` method is created if and only if there at least one member in the body that "requires" it. * `$init$` are otherwise never created nor removed. * A call to `T.$init$` in a subclass `C` is generated if and only if `T.$init$` exists. Scala 3 has a flags-based approach to the above: * At parsing time, a `<init>` method is always created for traits. * The `NoInits` flag is added if the body does not contain any member that "requires" an initialization. * In addition, the constructor receives the `StableRealizable` flag if the trait *and all its base classes/traits* have the `NoInits` flag. This is then used for purity analysis in the inliner. * The `Constructors` phase creates a `$init$` method for traits that do not have the `NoInits` flag, and generates calls based on the same criteria. Now, one might think that this is all fine, except it isn't when we mix Scala 2 and 3, and in particular when a Scala 2 class extend a Scala 3 trait. Indeed, since Scala 3 *always* defines a `<init>` method in traits, which Scala 2 translates as `$init$`, Scala 2 would think that it always needs to emit a call to `$init$` (even for traits where Scala 3 does not, in fact, emit a `$init$` method in the end). This was mitigated in the TASTy reader of Scala 2 by removing `$init$` if it has the `STABLE` flag (coming from `StableRealizable`). This would have been fine if `StableRealizable` was present if and only if the owner trait has `NoInits`. But until this commit, that was not the case: a trait without initialization in itself, but extending a trait with initialization code, would have the flag `NoInits` but its constructor would not have the `StableRealizable` flag. Therefore, this commit basically reconciles the `NoInits` and `StableRealizable` flags, so that Scala 2 understands whether or not a Scala 3 trait has a `$init$` method. We also align those flags when reading from Scala 2 traits, so that Scala 3 also understands whether or not a Scala 2 trait has a `$init$` trait. This solves the compatibility issue between Scala 2 and Scala 3. One detail remains. The attentive reader may have noticed the quotes in 'an element of the body that "requires" initialization'. Scala 2 and Scala 3 do not agree on what requires initialization: notably, Scala 2 thinks that a concrete `def` requires initialization, whereas Scala 3 knows that this is not the case. This is not an issue for synchronous interoperability between Scala 2 and 3, since each decides on its own which of its traits has a `$init$` method, and communicates that fact to the other version. However, this still poses an issue for "diachronic" compatibility: if a library defines a trait with a concrete `def` and is compiled by Scala 2 in version v1, that trait will have a `$init$`. When the library upgrades to Scala 3 in version v2, the trait will *lose* the `$init$`, which can break third-party subclasses. This commit does not address this issue. There are two ways we could do so: * Precisely align the detection of whether a trait should have a `$init$` method with Scala 2 (notably, adding one when there is a concrete `def`), or * *Always* emit an empty static `$init$` method, even for traits that have the `NoInits` flag (but do not generate calls to them, nor have that affect whether or not subclasses are considered pure).
c2f35b8
to
471fcd9
Compare
Scala 2 uses a very simple mechanism to detect traits that have or don't have a
$init$
method:$init$
method is created if and only if there at least one member in the body that "requires" it.$init$
are otherwise never created nor removed.T.$init$
in a subclassC
is generated if and only ifT.$init$
exists.Scala 3 has a flags-based approach to the above:
<init>
method is always created for traits.NoInits
flag is added if the body does not contain any member that "requires" an initialization.StableRealizable
flag if the trait and all its base classes/traits have theNoInits
flag. This is then used for purity analysis in the inliner.Constructors
phase creates a$init$
method for traits that do not have theNoInits
flag, and generates calls based on the same criteria.Now, one might think that this is all fine, except it isn't when we mix Scala 2 and 3, and in particular when a Scala 2 class extend a Scala 3 trait.
Indeed, since Scala 3 always defines a
<init>
method in traits, which Scala 2 translates as$init$
, Scala 2 would think that it always needs to emit a call to$init$
(even for traits where Scala 3 does not, in fact, emit a$init$
method in the end). This was mitigated in the TASTy reader of Scala 2 by removing$init$
if it has theSTABLE
flag (coming fromStableRealizable
).This would have been fine if
StableRealizable
was present if and only if the owner trait hasNoInits
. But until this commit, that was not the case: a trait without initialization in itself, but extending a trait with initialization code, would have the flagNoInits
but its constructor would not have theStableRealizable
flag.Therefore, this commit basically reconciles the
NoInits
andStableRealizable
flags, so that Scala 2 understands whether or not a Scala 3 trait has a$init$
method. We also align those flags when reading from Scala 2 traits, so that Scala 3 also understands whether or not a Scala 2 trait has a$init$
trait.This solves the compatibility issue between Scala 2 and Scala 3.
One detail remains. The attentive reader may have noticed the quotes in 'an element of the body that "requires" initialization'. Scala 2 and Scala 3 do not agree on what requires initialization: notably, Scala 2 thinks that a concrete
def
requires initialization, whereas Scala 3 knows that this is not the case. This is not an issue for synchronous interoperability between Scala 2 and 3, since each decides on its own which of its traits has a$init$
method, and communicates that fact to the other version.However, this still poses an issue for "diachronic" compatibility: if a library defines a trait with a concrete
def
and is compiled by Scala 2 in version v1, that trait will have a$init$
. When the library upgrades to Scala 3 in version v2, the trait will lose the$init$
, which can break third-party subclasses.This commit does not address this issue. There are two ways we could do so:
$init$
method with Scala 2 (notably, adding one when there is aconcrete
def
), or$init$
method, even for traits that have theNoInits
flag (but do not generate calls to them, nor have that affect whether or not subclasses are considered pure).