Skip to content

Commit 17e3f8b

Browse files
committed
fix #7227: allow custom toString on enum
productPrefix is now overriden using the enum constant's name and used in the by-name lookup in EnumValues. java based enum values are optimised so that productPrefix will forward to .name in the simple enum case, avoiding an extra field
1 parent 64a239f commit 17e3f8b

File tree

4 files changed

+95
-20
lines changed

4 files changed

+95
-20
lines changed

compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,21 @@ object DesugarEnums {
125125
/** A creation method for a value of enum type `E`, which is defined as follows:
126126
*
127127
* private def $new(_$ordinal: Int, $name: String) = new E with scala.runtime.EnumValue {
128-
* def ordinal = _$ordinal // if `E` does not derive from jl.Enum
129-
* override def toString = $name // if `E` does not derive from jl.Enum
128+
* def ordinal = _$ordinal // if `E` does not derive from jl.Enum
129+
* override def productPrefix = $name // if `E` does not derive from `java.lang.Enum`
130+
* override def productPrefix = this.name // if `E` derives from `java.lang.Enum`
130131
* $values.register(this)
131132
* }
132133
*/
133134
private def enumValueCreator(using Context) = {
134135
val fieldMethods =
135-
if isJavaEnum then Nil
136-
else
137-
val ordinalDef = ordinalMeth(Ident(nme.ordinalDollar_))
138-
val toStringDef = toStringMeth(Ident(nme.nameDollar))
139-
List(ordinalDef, toStringDef)
136+
if isJavaEnum then
137+
val productPrefixDef = productPrefixMeth(Select(This(Ident(tpnme.EMPTY)), nme.name))
138+
productPrefixDef :: Nil
139+
else
140+
val ordinalDef = ordinalMeth(Ident(nme.ordinalDollar_))
141+
val productPrefixDef = productPrefixMeth(Ident(nme.nameDollar))
142+
ordinalDef :: productPrefixDef :: Nil
140143
val creator = New(Template(
141144
constr = emptyConstructor,
142145
parents = enumClassRef :: scalaRuntimeDot(tpnme.EnumValue) :: Nil,
@@ -273,14 +276,14 @@ object DesugarEnums {
273276
def ordinalMeth(body: Tree)(using Context): DefDef =
274277
DefDef(nme.ordinal, Nil, Nil, TypeTree(defn.IntType), body)
275278

276-
def toStringMeth(body: Tree)(using Context): DefDef =
277-
DefDef(nme.toString_, Nil, Nil, TypeTree(defn.StringType), body).withFlags(Override)
279+
def productPrefixMeth(body: Tree)(using Context): DefDef =
280+
DefDef(nme.productPrefix, Nil, Nil, TypeTree(defn.StringType), body).withFlags(Override)
278281

279282
def ordinalMethLit(ord: Int)(using Context): DefDef =
280283
ordinalMeth(Literal(Constant(ord)))
281284

282-
def toStringMethLit(name: String)(using Context): DefDef =
283-
toStringMeth(Literal(Constant(name)))
285+
def productPrefixLit(name: String)(using Context): DefDef =
286+
productPrefixMeth(Literal(Constant(name)))
284287

285288
/** Expand a module definition representing a parameterless enum case */
286289
def expandEnumModule(name: TermName, impl: Template, mods: Modifiers, span: Span)(using Context): Tree = {
@@ -290,15 +293,11 @@ object DesugarEnums {
290293
expandSimpleEnumCase(name, mods, span)
291294
else {
292295
val (tag, scaffolding) = nextOrdinal(CaseKind.Object)
293-
val fieldMethods =
294-
if isJavaEnum then Nil
295-
else
296-
val ordinalDef = ordinalMethLit(tag)
297-
val toStringDef = toStringMethLit(name.toString)
298-
List(ordinalDef, toStringDef)
296+
val productPrefixDef = productPrefixLit(name.toString)
297+
val ordinalDef = if isJavaEnum then Nil else ordinalMethLit(tag) :: Nil
299298
val impl1 = cpy.Template(impl)(
300299
parents = impl.parents :+ scalaRuntimeDot(tpnme.EnumValue),
301-
body = fieldMethods ::: registerCall :: Nil)
300+
body = ordinalDef ::: productPrefixDef :: registerCall :: Nil)
302301
.withAttachment(ExtendsSingletonMirror, ())
303302
val vdef = ValDef(name, TypeTree(), New(impl1)).withMods(mods.withAddedFlags(EnumValue, span))
304303
flatTree(scaffolding ::: vdef :: Nil).withSpan(span)

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
5757
private var myValueSymbols: List[Symbol] = Nil
5858
private var myCaseSymbols: List[Symbol] = Nil
5959
private var myCaseModuleSymbols: List[Symbol] = Nil
60+
private var myEnumValueSymbols: List[Symbol] = Nil
6061

6162
private def initSymbols(using Context) =
6263
if (myValueSymbols.isEmpty) {
@@ -65,11 +66,13 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
6566
defn.Product_productArity, defn.Product_productPrefix, defn.Product_productElement,
6667
defn.Product_productElementName)
6768
myCaseModuleSymbols = myCaseSymbols.filter(_ ne defn.Any_equals)
69+
myEnumValueSymbols = List(defn.Any_toString)
6870
}
6971

7072
def valueSymbols(using Context): List[Symbol] = { initSymbols; myValueSymbols }
7173
def caseSymbols(using Context): List[Symbol] = { initSymbols; myCaseSymbols }
7274
def caseModuleSymbols(using Context): List[Symbol] = { initSymbols; myCaseModuleSymbols }
75+
def enumValueSymbols(using Context): List[Symbol] = { initSymbols; myEnumValueSymbols }
7376

7477
private def existingDef(sym: Symbol, clazz: ClassSymbol)(using Context): Symbol = {
7578
val existing = sym.matchingMember(clazz.thisType)
@@ -89,11 +92,17 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
8992
if (isDerivedValueClass(clazz)) clazz.paramAccessors.take(1) // Tail parameters can only be `erased`
9093
else clazz.caseAccessors
9194
val isEnumCase = clazz.derivesFrom(defn.EnumClass) && clazz != defn.EnumClass
95+
val isNonJavaEnumValue =
96+
isEnumCase
97+
&& clazz.isAnonymousClass
98+
&& clazz.classParents.head.classSymbol.is(Enum)
99+
&& !clazz.derivesFrom(defn.JavaEnumClass)
92100

93101
val symbolsToSynthesize: List[Symbol] =
94102
if (clazz.is(Case))
95103
if (clazz.is(Module)) caseModuleSymbols
96104
else caseSymbols
105+
else if (isNonJavaEnumValue) enumValueSymbols
97106
else if (isDerivedValueClass(clazz)) valueSymbols
98107
else Nil
99108

@@ -113,10 +122,18 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
113122
def ownName: Tree =
114123
Literal(Constant(clazz.name.stripModuleClassSuffix.toString))
115124

125+
def callProductPrefix: Tree =
126+
Select(This(clazz), nme.productPrefix).ensureApplied
127+
128+
def toStringBody(vrefss: List[List[Tree]]): Tree =
129+
if (clazz.is(ModuleClass)) ownName
130+
else if (isNonJavaEnumValue) callProductPrefix
131+
else forwardToRuntime(vrefss.head)
132+
116133
def syntheticRHS(vrefss: List[List[Tree]])(using Context): Tree = synthetic.name match {
117134
case nme.hashCode_ if isDerivedValueClass(clazz) => valueHashCodeBody
118135
case nme.hashCode_ => chooseHashcode
119-
case nme.toString_ => if (clazz.is(ModuleClass)) ownName else forwardToRuntime(vrefss.head)
136+
case nme.toString_ => toStringBody(vrefss)
120137
case nme.equals_ => equalsBody(vrefss.head.head)
121138
case nme.canEqual_ => canEqualBody(vrefss.head.head)
122139
case nme.productArity => Literal(Constant(accessors.length))

library/src/scala/runtime/EnumValues.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ class EnumValues[E <: Enum] {
1414

1515
def fromInt: Map[Int, E] = myMap
1616
def fromName: Map[String, E] = {
17-
if (fromNameCache == null) fromNameCache = myMap.values.map(v => v.toString -> v).toMap
17+
// TODO remove cast when scala.Enum is bootstrapped
18+
if (fromNameCache == null) fromNameCache = myMap.values.map(v => v.asInstanceOf[Product].productPrefix -> v).toMap
1819
fromNameCache
1920
}
2021
def values: Iterable[E] = myMap.values

tests/run/enum-custom-toString.scala

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
enum ES:
2+
case A
3+
override def toString: String = "overridden"
4+
5+
enum EJ extends java.lang.Enum[EJ]:
6+
case B
7+
override def toString: String = "overridden"
8+
9+
trait Mixin:
10+
override def toString: String = "overridden"
11+
12+
enum EM extends Mixin:
13+
case C
14+
15+
enum ET[T] extends java.lang.Enum[ET[_]]:
16+
case D extends ET[Unit]
17+
override def toString: String = "overridden"
18+
19+
enum EZ:
20+
case E(arg: Int)
21+
override def toString: String = "overridden"
22+
23+
enum EC: // control case
24+
case F
25+
case G(arg: Int)
26+
27+
abstract class Tag[T] extends Enum
28+
object Tag:
29+
private final class IntTagImpl extends Tag[Int] with runtime.EnumValue:
30+
def ordinal = 0
31+
override def hashCode = 123
32+
final val IntTag: Tag[Int] = IntTagImpl()
33+
34+
@main def Test =
35+
assert(ES.A.toString == "overridden", s"ES.A.toString = ${ES.A.toString}")
36+
assert(ES.A.productPrefix == "A", s"ES.A.productPrefix = ${ES.A.productPrefix}")
37+
assert(ES.valueOf("A") == ES.A, s"ES.valueOf(A) = ${ES.valueOf("A")}")
38+
assert(EJ.B.toString == "overridden", s"EJ.B.toString = ${EJ.B.toString}")
39+
assert(EJ.B.productPrefix == "B", s"EJ.B.productPrefix = ${EJ.B.productPrefix}")
40+
assert(EJ.valueOf("B") == EJ.B, s"EJ.valueOf(B) = ${EJ.valueOf("B")}")
41+
assert(EM.C.toString == "overridden", s"EM.C.toString = ${EM.C.toString}")
42+
assert(EM.C.productPrefix == "C", s"EM.C.productPrefix = ${EM.C.productPrefix}")
43+
assert(EM.valueOf("C") == EM.C, s"EM.valueOf(C) = ${EM.valueOf("C")}")
44+
assert(ET.D.toString == "overridden", s"ET.D.toString = ${ET.D.toString}")
45+
assert(ET.D.productPrefix == "D", s"ET.D.productPrefix = ${ET.D.productPrefix}")
46+
assert(EZ.E(0).toString == "overridden", s"EZ.E(0).toString = ${EZ.E(0).toString}")
47+
assert(EZ.E(0).productPrefix == "E", s"EZ.E(0).productPrefix = ${EZ.E(0).productPrefix}")
48+
assert(EC.F.toString == "F", s"EC.F.toString = ${EC.F.toString}")
49+
assert(EC.F.productPrefix == "F", s"EC.F.productPrefix = ${EC.F.productPrefix}")
50+
assert(EC.valueOf("F") == EC.F, s"EC.valueOf(F) = ${EC.valueOf("F")}")
51+
assert(EC.G(0).toString == "G(0)", s"EC.G(0).toString = ${EC.G(0).toString}")
52+
assert(EC.G(0).productPrefix == "G", s"EC.G(0).productPrefix = ${EC.G(0).productPrefix}")
53+
54+
assert(
55+
assertion = Tag.IntTag.toString == s"${Tag.IntTag.getClass.getName}@${Integer.toHexString(123)}",
56+
message = s"Tag.IntTag.toString = ${Tag.IntTag.toString}"
57+
)
58+
assert(Tag.IntTag.productPrefix == Tag.IntTag.toString, s"Tag.IntTag.productPrefix = ${Tag.IntTag.productPrefix}")

0 commit comments

Comments
 (0)