Compiler version
Regression between Scala 3.9.0-RC4 and 3.9.0-RC5.
Minimized code
// Library.scala
trait T:
object Nested
// Consumer.scala
class C extends T
@main def run(): Unit =
new C
Compile both files with Scala 3.9.0-RC4. The generated constructor of C invokes:
Then recompile only Library.scala with Scala 3.9.0-RC5 while retaining the RC4-compiled C.class.
Instantiating C can now fail with:
java.lang.NoSuchMethodError: 'void T.$init$(T)'
at C.<init>(Consumer.scala:1)
Bytecode difference
With RC4:
public interface T {
public static void $init$(T);
public static T$Nested$ Nested$(T);
public default T$Nested$ Nested();
}
The generated $init$ is empty:
public static void $init$(T);
Code:
0: return
With RC5:
public interface T {
public static T$Nested$ Nested$(T);
public default T$Nested$ Nested();
}
Thus RC5 removes a method that existing downstream bytecode may invoke. This is a real JVM binary incompatibility rather than only a difference in synthetic metadata.
Real-world occurrence
This was found while upgrading Play Framework from RC4 to RC5.: https://github.com/playframework/playframework/actions/runs/31740913294/job/94585571944?pr=14181
MiMa reports:
[error] Play: Failed binary compatibility check against org.playframework:play_3:3.0.0! Found 3 potential problems (filtered 242)
[error] * synthetic static method $init$(play.api.mvc.AcceptExtractors)Unit in interface play.api.mvc.AcceptExtractors does not have a correspondent in current version
[error] filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.mvc.AcceptExtractors.$init$")
[error] * synthetic static method $init$(play.api.mvc.Rendering)Unit in interface play.api.mvc.Rendering does not have a correspondent in current version
[error] filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.mvc.Rendering.$init$")
[error] * synthetic static method $init$(play.api.mvc.RequestExtractors)Unit in interface play.api.mvc.RequestExtractors does not have a correspondent in current version
[error] filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.mvc.RequestExtractors.$init$")
These three traits contain nested objects but no other initialization:
AcceptExtractors contains object Accepts
RequestExtractors contains object &
Rendering contains object render
Comparing the RC4 and RC5 classfiles with javap shows that removal of $init$ is the relevant public-signature difference.
Suspected cause
The causative change appears to be #26579, specifically:
case _: ModuleDef => NoInits
Release-branch commit:
The same patch/tree also appears as 1faf08b because of the RC5 backport topology.
PR #26579 intentionally preserves NoInits when an enclosing definition contains a nested object:
However, the motivating issue concerns purity analysis for an enclosing object. Its test also uses an object, not a trait. The resulting removal of trait $init$ methods and its binary-compatibility impact do not appear to be discussed or tested in that PR.
There is relevant historical context in #2081 and #3712, where the compiler was changed not to emit $init$ for NoInits traits:
The discussion in #2081 explicitly notes that adding or removing $init$ is a real bytecode incompatibility.
Expected behavior (?)
Upgrading the compiler within the same Scala 3 binary ecosystem should not remove a trait $init$ method that previously compiled downstream classes can invoke.
Possible solutions might include:
- continuing to emit an empty
$init$ for these traits; or
- limiting the new nested-
object NoInits handling so it does not alter the ABI of traits.
At minimum, it would be helpful to confirm whether this binary incompatibility was intended before Scala 3.9.0 final.
Note
fyi, I generated this report with codex.
Upgrading Play from RC4 to RC5 fails - my question bascially is if the binary incompatibility was introdcued on purpose: https://github.com/playframework/playframework/actions/runs/31740913294/job/94585571944?pr=14181#step:7:1274
Compiler version
Regression between Scala
3.9.0-RC4and3.9.0-RC5.Minimized code
Compile both files with Scala 3.9.0-RC4. The generated constructor of
Cinvokes:Then recompile only
Library.scalawith Scala 3.9.0-RC5 while retaining the RC4-compiledC.class.Instantiating
Ccan now fail with:Bytecode difference
With RC4:
The generated
$init$is empty:With RC5:
Thus RC5 removes a method that existing downstream bytecode may invoke. This is a real JVM binary incompatibility rather than only a difference in synthetic metadata.
Real-world occurrence
This was found while upgrading Play Framework from RC4 to RC5.: https://github.com/playframework/playframework/actions/runs/31740913294/job/94585571944?pr=14181
MiMa reports:
These three traits contain nested objects but no other initialization:
AcceptExtractorscontainsobject AcceptsRequestExtractorscontainsobject &Renderingcontainsobject renderComparing the RC4 and RC5 classfiles with
javapshows that removal of$init$is the relevant public-signature difference.Suspected cause
The causative change appears to be #26579, specifically:
Release-branch commit:
The same patch/tree also appears as 1faf08b because of the RC5 backport topology.
PR #26579 intentionally preserves
NoInitswhen an enclosing definition contains a nested object:Any_typeCast) hide effects from purity analysis #26578However, the motivating issue concerns purity analysis for an enclosing
object. Its test also uses anobject, not a trait. The resulting removal of trait$init$methods and its binary-compatibility impact do not appear to be discussed or tested in that PR.There is relevant historical context in #2081 and #3712, where the compiler was changed not to emit
$init$forNoInitstraits:The discussion in #2081 explicitly notes that adding or removing
$init$is a real bytecode incompatibility.Expected behavior (?)
Upgrading the compiler within the same Scala 3 binary ecosystem should not remove a trait
$init$method that previously compiled downstream classes can invoke.Possible solutions might include:
$init$for these traits; orobjectNoInitshandling so it does not alter the ABI of traits.At minimum, it would be helpful to confirm whether this binary incompatibility was intended before Scala 3.9.0 final.