Skip to content

Commit 4bc8cb7

Browse files
committed
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$.<clinit>` 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.<clinit>` is triggered - `Testme.<clinit>` tries to initialize its `Testme.Hello` field by pulling from `Testme$.Hello` - However, it's still null during the companion's static initialization! See: #12637 (comment) ```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 `<clinit>` runs and creates enum values - During initialization, the forwarder's `<clinit>` is triggered - Forwarders pull from the companion (value will be null) - Companion's `<clinit>` pushes final values to forwarders at the end **When the forwarder is accessed first:** - Enum class's `<clinit>` tries to initialize the forwarder via `getstatic` from the companion - This triggers the companion's `<clinit>` first - Companion's `<clinit>` 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**
1 parent 48cad7c commit 4bc8cb7

File tree

1 file changed

+41
-3
lines changed

1 file changed

+41
-3
lines changed

compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
106106
val moduleRef = ref(clazz.companionModule)
107107

108108
val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue))
109-
for { enumValue <- enums }
109+
val forwarderSyms = scala.collection.mutable.ListBuffer[Symbol]()
110+
111+
val result = for { enumValue <- enums }
110112
yield {
111113
def forwarderSym(flags: FlagSet, info: Type): Symbol { type ThisName = TermName } =
112114
val sym = newSymbol(clazz, enumValue.name.asTermName, flags, info)
@@ -119,8 +121,36 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
119121
// we achieve the right contract with static forwarders instead.
120122
DefDef(forwarderSym(EnumValue | Method | JavaStatic, MethodType(Nil, enumValue.info)), body)
121123
else
122-
ValDef(forwarderSym(EnumValue | JavaStatic, enumValue.info), body)
124+
val sym = forwarderSym(EnumValue | JavaStatic | Mutable, enumValue.info)
125+
forwarderSyms += sym
126+
ValDef(sym, body)
123127
}
128+
129+
// Store forwarder symbols for later use in companion initialization
130+
if forwarderSyms.nonEmpty then
131+
enumForwarders(clazz) = forwarderSyms.toList
132+
133+
result
134+
}
135+
136+
/** Generate assignment to initialize enum forwarders in the companion object,
137+
* so that forwarders are initialized when comapnion object is touched first.
138+
* For each enum value, generates: EnumClass.enumValue = Module.enumValue
139+
* see: https://github.com/scala/scala3/issues/12637
140+
*/
141+
private def enumForwarderInitializers(moduleCls: Symbol)(using Context): List[Tree] = {
142+
if ctx.settings.scalajs.value then
143+
Nil // Scala.js uses methods, no initialization needed
144+
else
145+
val enumClass = moduleCls.linkedClass
146+
val forwarderSyms = enumForwarders.get(enumClass).getOrElse(Nil)
147+
val enums = moduleCls.info.decls.filter(member => member.isAllOf(EnumValue)).toList
148+
149+
forwarderSyms.zip(enums).map { case (forwarderSym, enumValue) =>
150+
val lhs = ref(forwarderSym)
151+
val rhs = ref(enumValue)
152+
Assign(lhs, rhs)
153+
}
124154
}
125155

126156
private def isJavaEnumValueImpl(cls: Symbol)(using Context): Boolean =
@@ -129,6 +159,7 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
129159
&& cls.owner.owner.linkedClass.derivesFromJavaEnum
130160

131161
private val enumCaseOrdinals = MutableSymbolMap[Int]()
162+
private val enumForwarders = MutableSymbolMap[List[Symbol]]()
132163

133164
private def registerEnumClass(cls: Symbol)(using Context): Unit =
134165
cls.children.zipWithIndex.foreach(enumCaseOrdinals.update)
@@ -181,10 +212,17 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
181212
)
182213
else if cls.linkedClass.derivesFromJavaEnum then
183214
enumCaseOrdinals.clear() // remove simple cases // invariant: companion is visited after cases
184-
templ
215+
// Add initialization code for enum forwarders
216+
val initializers = enumForwarderInitializers(cls)
217+
enumForwarders.remove(cls.linkedClass) // Clear cache after use
218+
if initializers.isEmpty then
219+
templ
220+
else
221+
cpy.Template(templ)(body = templ.body ++ initializers)
185222
else templ
186223
}
187224

188225
override def checkPostCondition(tree: Tree)(using Context): Unit =
189226
assert(enumCaseOrdinals.isEmpty, "Java based enum ordinal cache was not cleared")
227+
assert(enumForwarders.isEmpty, "Java based enum forwarder cache was not cleared")
190228
}

0 commit comments

Comments
 (0)