From 48cad7cc78f36bfdd2b44e4bbf9fcf7d1673a33a Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Fri, 7 Nov 2025 15:33:08 +0900 Subject: [PATCH 1/3] Add failing test for #12637 --- compiler/test/dotc/run-test-pickling.excludelist | 1 + tests/run/enum-java-scala.check | 2 ++ tests/run/enum-java-scala/Test.java | 7 +++++++ tests/run/enum-java-scala/Testme.scala | 5 +++++ 4 files changed, 15 insertions(+) create mode 100644 tests/run/enum-java-scala.check create mode 100644 tests/run/enum-java-scala/Test.java create mode 100644 tests/run/enum-java-scala/Testme.scala diff --git a/compiler/test/dotc/run-test-pickling.excludelist b/compiler/test/dotc/run-test-pickling.excludelist index 0122276739c0..d0efb4ba4bd5 100644 --- a/compiler/test/dotc/run-test-pickling.excludelist +++ b/compiler/test/dotc/run-test-pickling.excludelist @@ -4,6 +4,7 @@ derive-generic.scala eff-dependent.scala enum-java +enum-java-scala i5257.scala i7212 i7868.scala diff --git a/tests/run/enum-java-scala.check b/tests/run/enum-java-scala.check new file mode 100644 index 000000000000..daa498773f34 --- /dev/null +++ b/tests/run/enum-java-scala.check @@ -0,0 +1,2 @@ +Scala: Testme Hello= Hello +Java: Testme Hello= Hello \ No newline at end of file diff --git a/tests/run/enum-java-scala/Test.java b/tests/run/enum-java-scala/Test.java new file mode 100644 index 000000000000..cf2ab3802845 --- /dev/null +++ b/tests/run/enum-java-scala/Test.java @@ -0,0 +1,7 @@ +// see: https://github.com/scala/scala3/issues/12637 +public class Test { + public static void main(String[] args) { + TestenumS.go(); + System.out.println("Java: Testme Hello= " + Testme.Hello); + } +} diff --git a/tests/run/enum-java-scala/Testme.scala b/tests/run/enum-java-scala/Testme.scala new file mode 100644 index 000000000000..cf8b1e0abe36 --- /dev/null +++ b/tests/run/enum-java-scala/Testme.scala @@ -0,0 +1,5 @@ +object TestenumS: + def go() = println("Scala: Testme Hello= " + Testme.Hello) + +enum Testme extends java.lang.Enum[Testme]: + case Hello From 4bc8cb7292e1c2ed1b986b7e3332ff225dad1ee2 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Fri, 7 Nov 2025 19:00:32 +0900 Subject: [PATCH 2/3] Fix Java enum forwarders to pull from companion on initialization Fixes #12637 Previously, Java enum forwarders were initialized to null when the companion's static initializer was triggered before the Java enum forwarder class's static initializer. For example, when `enum Testme extends java.lang.Enum[Testme]` is accessed from Scala (which accesses `Testme$.Hello`): - `Testme$.` is triggered - The static initializer creates enum values by calling `Testme$.new(...)` - It constructs `Testme$$anon$1` (which represents `Hello`), a subtype of `Testme` - Therefore, `Testme.` is triggered - `Testme.` tries to initialize its `Testme.Hello` field by pulling from `Testme$.Hello` - However, it's still null during the companion's static initialization! See: https://github.com/scala/scala3/issues/12637#issuecomment-3481107947 ```scala // Testme.scala object TestenumS: def go() = println("Scala: Testme Hello= " + Testme.Hello) enum Testme extends java.lang.Enum[Testme]: case Hello // TestenumJ.java public class TestenumJ { public static void main(String[] args) { TestenumS.go(); System.out.println("Java: Testme Hello= " + Testme.Hello); } } ``` This commit fixes the initialization problem by having the companion object's static initializer push enum values to the forwarders after it finishes initializing the enum value fields. **When the companion is accessed first:** - Companion's `` runs and creates enum values - During initialization, the forwarder's `` is triggered - Forwarders pull from the companion (value will be null) - Companion's `` pushes final values to forwarders at the end **When the forwarder is accessed first:** - Enum class's `` tries to initialize the forwarder via `getstatic` from the companion - This triggers the companion's `` first - Companion's `` pushes values to the forwarders - The original `putstatic` completes (resulting in double assignment, but with the correct value) **Drawbacks:** - We assign the forwarder field twice, making it slightly slower than before - **We changed the Java enum forwarder fields to be non-final** --- .../dotc/transform/CompleteJavaEnums.scala | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala b/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala index 5740f359cb77..44759ec9a412 100644 --- a/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala +++ b/compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala @@ -106,7 +106,9 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase => val moduleRef = ref(clazz.companionModule) val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue)) - for { enumValue <- enums } + val forwarderSyms = scala.collection.mutable.ListBuffer[Symbol]() + + val result = for { enumValue <- enums } yield { def forwarderSym(flags: FlagSet, info: Type): Symbol { type ThisName = TermName } = val sym = newSymbol(clazz, enumValue.name.asTermName, flags, info) @@ -119,8 +121,36 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase => // we achieve the right contract with static forwarders instead. DefDef(forwarderSym(EnumValue | Method | JavaStatic, MethodType(Nil, enumValue.info)), body) else - ValDef(forwarderSym(EnumValue | JavaStatic, enumValue.info), body) + val sym = forwarderSym(EnumValue | JavaStatic | Mutable, enumValue.info) + forwarderSyms += sym + ValDef(sym, body) } + + // Store forwarder symbols for later use in companion initialization + if forwarderSyms.nonEmpty then + enumForwarders(clazz) = forwarderSyms.toList + + result + } + + /** Generate assignment to initialize enum forwarders in the companion object, + * so that forwarders are initialized when comapnion object is touched first. + * For each enum value, generates: EnumClass.enumValue = Module.enumValue + * see: https://github.com/scala/scala3/issues/12637 + */ + private def enumForwarderInitializers(moduleCls: Symbol)(using Context): List[Tree] = { + if ctx.settings.scalajs.value then + Nil // Scala.js uses methods, no initialization needed + else + val enumClass = moduleCls.linkedClass + val forwarderSyms = enumForwarders.get(enumClass).getOrElse(Nil) + val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue)).toList + + forwarderSyms.zip(enums).map { case (forwarderSym, enumValue) => + val lhs = ref(forwarderSym) + val rhs = ref(enumValue) + Assign(lhs, rhs) + } } private def isJavaEnumValueImpl(cls: Symbol)(using Context): Boolean = @@ -129,6 +159,7 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase => && cls.owner.owner.linkedClass.derivesFromJavaEnum private val enumCaseOrdinals = MutableSymbolMap[Int]() + private val enumForwarders = MutableSymbolMap[List[Symbol]]() private def registerEnumClass(cls: Symbol)(using Context): Unit = cls.children.zipWithIndex.foreach(enumCaseOrdinals.update) @@ -181,10 +212,17 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase => ) else if cls.linkedClass.derivesFromJavaEnum then enumCaseOrdinals.clear() // remove simple cases // invariant: companion is visited after cases - templ + // Add initialization code for enum forwarders + val initializers = enumForwarderInitializers(cls) + enumForwarders.remove(cls.linkedClass) // Clear cache after use + if initializers.isEmpty then + templ + else + cpy.Template(templ)(body = templ.body ++ initializers) else templ } override def checkPostCondition(tree: Tree)(using Context): Unit = assert(enumCaseOrdinals.isEmpty, "Java based enum ordinal cache was not cleared") + assert(enumForwarders.isEmpty, "Java based enum forwarder cache was not cleared") } From 6d8bc37c0f8808cb6db82fa7b80f845b81796453 Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Fri, 7 Nov 2025 21:32:26 +0900 Subject: [PATCH 3/3] Skip scala.js for enum-java-scala test --- tests/run/enum-java-scala/Test.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/run/enum-java-scala/Test.java b/tests/run/enum-java-scala/Test.java index cf2ab3802845..7d059e592098 100644 --- a/tests/run/enum-java-scala/Test.java +++ b/tests/run/enum-java-scala/Test.java @@ -1,3 +1,5 @@ +// scalajs: --skip + // see: https://github.com/scala/scala3/issues/12637 public class Test { public static void main(String[] args) {