-
Notifications
You must be signed in to change notification settings - Fork 1.1k
No static $init$ in pure trait extending impure trait #9970
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
Comments
Additionally I guess classes extending |
The That would mean that the computation of the |
Hmm. But there are no initializers; the $init$method is empty. So |
There is no initializer in |
its not that simple because in this case IOApp.Simple should not get an initialiser: trait IOApp {
val foo = 23
}
object IOApp {
trait Simple extends IOApp
} |
i refer to my comment above that when IOApp and Simple defines no methods Simple should not get an init |
Some notes about my investigation so far:
For Scala 2 interop, we need to generate a
We may, if necessary:
I don't think those "may"s would help the implementation, though. So I would stick with generate-and-call That means that we sometimes must not generate an Consequently, we need to decouple the generation of |
@bishabosha What does the Tasty reader in 2.13 do about this? Does it try to ignore the |
@sjrd Currently in Scala 2, any |
Thanks. What do you mean by "if it is stable"? I was not aware that a constructor could ever be stable. |
just if you print out the tasty, you will see that the constructor has the stable flag |
Sorry, I actually meant that if the constructor has a stable flag then it will not be entered |
Oh OK. So yes, apparently class and mixin constructors receive the |
@bishabosha How could I write a test for the original issue? Is my only option to write a scripted test, or do we have better now? |
I think thats the only option, without creating your own test framework |
Alternatively, I can propose a bytecode test/java reflection test where you curate some examples based on what scala 2 does, without comparing to scala 2 code |
That wouldn't work in the solution that I have, because eventually I found a way not to completely align with what Scala 2 does, while still correctly communicate to Scala 2 what to do with our traits. |
…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).
…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).
Ah! I can't even write a scripted test, because I would need to use Scala 2.13.4 with |
…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).
…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).
…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).
…cala2 Fix #9970: Reconcile how Scala 3 and 2 decide whether a trait has $init$.
Minimized code
Output
IOApp compiled by dotty
0.27.0-RC1
:Expectation
IOApp compiled by
Scala 2.13.3
:This means we can't use cats-effect-3 (compiled by dotty) in scala 2.13, as extending
IOApp.Simple
expects the staticIOApp.Simple.$init$
method to existThe text was updated successfully, but these errors were encountered: