Skip to content

fix #9873: no longer use scala.Enum as parents of enums #9877

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ object desugar {
case _ => false
}

def isRetractableCaseClassOrEnumMethodName(name: Name)(using Context): Boolean =
isRetractableCaseClassMethodName(name) || name == nme.ordinal

/** Is `name` the name of a method that is added unconditionally to case classes? */
def isDesugaredCaseClassMethodName(name: Name)(using Context): Boolean =
isRetractableCaseClassMethodName(name) || name.isSelectorName
Expand Down Expand Up @@ -403,6 +406,7 @@ object desugar {
val isCaseObject = mods.is(Case) && isObject
val isEnum = mods.isEnumClass && !mods.is(Module)
def isEnumCase = mods.isEnumCase
def isNonEnumCase = !isEnumCase && (isCaseClass || isCaseObject)
val isValueClass = parents.nonEmpty && isAnyVal(parents.head)
// This is not watertight, but `extends AnyVal` will be replaced by `inline` later.

Expand Down Expand Up @@ -483,7 +487,9 @@ object desugar {
val enumCompanionRef = TermRefTree()
val enumImport =
Import(enumCompanionRef, enumCases.flatMap(caseIds).map(ImportSelector(_)))
(enumImport :: enumStats, enumCases, enumCompanionRef)
val enumLabelDef = DesugarEnums.enumLabelMeth(EmptyTree)
val ordinalDef = DesugarEnums.ordinalMeth(EmptyTree)
(enumImport :: ordinalDef :: enumLabelDef :: enumStats, enumCases, enumCompanionRef)
}
else (stats, Nil, EmptyTree)
}
Expand Down Expand Up @@ -621,10 +627,8 @@ object desugar {
var parents1 = parents
if (isEnumCase && parents.isEmpty)
parents1 = enumClassTypeRef :: Nil
if (isCaseClass | isCaseObject)
if (isNonEnumCase || isEnum)
parents1 = parents1 :+ scalaDot(str.Product.toTypeName) :+ scalaDot(nme.Serializable.toTypeName)
if (isEnum)
parents1 = parents1 :+ ref(defn.EnumClass.typeRef)

// derived type classes of non-module classes go to their companions
val (clsDerived, companionDerived) =
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/DesugarEnums.scala
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,10 @@ object DesugarEnums {
private def isJavaEnum(using Context): Boolean = enumClass.derivesFrom(defn.JavaEnumClass)

def ordinalMeth(body: Tree)(using Context): DefDef =
DefDef(nme.ordinal, Nil, Nil, TypeTree(defn.IntType), body)
DefDef(nme.ordinal, Nil, Nil, TypeTree(defn.IntType), body).withAddedFlags(Synthetic)

def enumLabelMeth(body: Tree)(using Context): DefDef =
DefDef(nme.enumLabel, Nil, Nil, TypeTree(defn.StringType), body)
DefDef(nme.enumLabel, Nil, Nil, TypeTree(defn.StringType), body).withAddedFlags(Synthetic)

def ordinalMethLit(ord: Int)(using Context): DefDef =
ordinalMeth(Literal(Constant(ord)))
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ object SymUtils {
self
}

def isEnum(using Context): Boolean = self.is(Enum, butNot=JavaDefined)
def isEnumClass(using Context): Boolean = isEnum && !self.is(Case)

/** Does this symbol refer to anonymous classes synthesized by enum desugaring? */
def isEnumAnonymClass(using Context): Boolean =
self.isAnonymousClass && (self.owner.name.eq(nme.DOLLAR_NEW) || self.owner.is(CaseVal))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
lazy val accessors =
if (isDerivedValueClass(clazz)) clazz.paramAccessors.take(1) // Tail parameters can only be `erased`
else clazz.caseAccessors
val isEnumCase = clazz.derivesFrom(defn.EnumClass) && clazz != defn.EnumClass
val isEnumValue = isEnumCase && clazz.isAnonymousClass && clazz.classParents.head.classSymbol.is(Enum)
val isEnumValue = clazz.isAnonymousClass && clazz.classParents.head.classSymbol.is(Enum)
val isNonJavaEnumValue = isEnumValue && !clazz.derivesFrom(defn.JavaEnumClass)

val symbolsToSynthesize: List[Symbol] =
Expand Down
81 changes: 64 additions & 17 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,13 @@ class Namer { typer: Typer =>
/** Expand tree and create top-level symbols for statement and enter them into symbol table */
def index(stat: Tree)(using Context): Context = {
expand(stat)
indexExpanded(stat)
indexExpanded(stat, mutable.Buffer.empty) // here we do not expect to populate the buffer
}

/** Create top-level symbols for all statements in the expansion of this statement and
* enter them into symbol table
*/
def indexExpanded(origStat: Tree)(using Context): Context = {
def indexExpanded(origStat: Tree, delayedEntry: mutable.Buffer[Context ?=> Context])(using Context): Context = {
def recur(stat: Tree): Context = stat match {
case pcl: PackageDef =>
val pkg = createPackageSymbol(pcl.pid)
Expand All @@ -395,10 +395,23 @@ class Namer { typer: Typer =>
ctx.importContext(imp, createSymbol(imp))
case mdef: DefTree =>
val sym = createSymbol(mdef)
enterSymbol(sym)
setDocstring(sym, origStat)
addEnumConstants(mdef, sym)
ctx
def enterDefTree(sym: Symbol): Context =
enterSymbol(sym)
setDocstring(sym, origStat)
addEnumConstants(mdef, sym)
ctx
if sym.is(Synthetic) && isEnumOrdinal(sym) then
delayedEntry += { ctx ?=>
// Here we delay entering into scope of an enum ordinal method until after
// the parents of `sym.owner` are known. This is because it is illegal to override ordinal
// in java.lang.Enum
if !clashingJavaEnumOrdinal(sym) then
enterDefTree(sym)
ctx
}
ctx
else
enterDefTree(sym)
case stats: Thicket =>
stats.toList.foreach(recur)
ctx
Expand Down Expand Up @@ -456,7 +469,10 @@ class Namer { typer: Typer =>
/** Create top-level symbols for statements and enter them into symbol table
* @return A context that reflects all imports in `stats`.
*/
def index(stats: List[Tree])(using Context): Context = {
def index(stats: List[Tree])(using Context): Context =
index(stats, mutable.Buffer.empty) // here we do not expect to fill the buffer

def index(stats: List[Tree], delayedEntry: mutable.Buffer[Context ?=> Context])(using Context): Context = {

// module name -> (stat, moduleCls | moduleVal)
val moduleClsDef = mutable.Map[TypeName, (Tree, TypeDef)]()
Expand Down Expand Up @@ -631,7 +647,7 @@ class Namer { typer: Typer =>

stats.foreach(expand)
mergeCompanionDefs()
val ctxWithStats = stats.foldLeft(ctx)((ctx, stat) => indexExpanded(stat)(using ctx))
val ctxWithStats = stats.foldLeft(ctx)((ctx, stat) => indexExpanded(stat, delayedEntry)(using ctx))
createCompanionLinks(using ctxWithStats)
ctxWithStats
}
Expand Down Expand Up @@ -663,6 +679,23 @@ class Namer { typer: Typer =>
sym.resetFlag(GivenOrImplicit)
}

private def findMatch(denot: SymDenotation, owner: Symbol)(using Context) =
owner.info.decls.lookupAll(denot.name).exists(alt =>
alt != denot.symbol && alt.info.matchesLoosely(denot.info))

private def isEnumOrdinal(denot: SymDenotation)(using Context) =
denot.name == nme.ordinal
&& denot.owner.isClass && denot.owner.isEnumClass

private def clashingJavaEnumOrdinal(denot: SymDenotation)(using Context) =
def firstParentCls(owner: Symbol) =
owner.asClass.classParents.head.classSymbol
def isJavaEnumBaseClass(owner: Symbol) =
owner.isClass && owner.isEnumClass && owner.derivesFrom(defn.JavaEnumClass)
denot.name == nme.ordinal
&& isJavaEnumBaseClass(denot.owner)
&& findMatch(denot, firstParentCls(denot.owner))

/** The completer of a symbol defined by a member def or import (except ClassSymbols) */
class Completer(val original: Tree)(ictx: Context) extends LazyType with SymbolLoaders.SecondCompleter {

Expand Down Expand Up @@ -743,21 +776,23 @@ class Namer { typer: Typer =>
}

/** Invalidate `denot` by overwriting its info with `NoType` if
* `denot` is a compiler generated case class method that clashes
* with a user-defined method in the same scope with a matching type.
* one of the following holds:
* - `denot` is a compiler generated case class method that clashes
* with a user-defined method in the same scope with a matching type.
* - `denot` is a compiler generated `ordinal` method that would override
* `ordinal` declared in `java.lang.Enum`
*/
private def invalidateIfClashingSynthetic(denot: SymDenotation): Unit = {
def isCaseClass(owner: Symbol) =
owner.isClass && {
if (owner.is(Module)) owner.linkedClass.is(CaseClass)
else owner.is(CaseClass)
}
val isClashingSynthetic =
denot.is(Synthetic) &&
desugar.isRetractableCaseClassMethodName(denot.name) &&
isCaseClass(denot.owner) &&
denot.owner.info.decls.lookupAll(denot.name).exists(alt =>
alt != denot.symbol && alt.info.matchesLoosely(denot.info))
def clashingCaseClassMethod =
desugar.isRetractableCaseClassMethodName(denot.name)
&& isCaseClass(denot.owner)
&& findMatch(denot, denot.owner)
val isClashingSynthetic = denot.is(Synthetic) && (clashingCaseClassMethod || clashingJavaEnumOrdinal(denot))
if (isClashingSynthetic) {
typr.println(i"invalidating clashing $denot in ${denot.owner}")
denot.markAbsent()
Expand Down Expand Up @@ -923,6 +958,11 @@ class Namer { typer: Typer =>
/** info to be used temporarily while completing the class, to avoid cyclic references. */
private var tempInfo: TempClassInfo = _

/** we delay entry of the following symbols (already created) until after parents are known:
* - def ordinal: Int (if parent is not java.lang.Enum)
*/
private var delayedEntry: List[Context ?=> Context] = _

val TypeDef(name, impl @ Template(constr, _, self, _)) = original

private val (params, rest): (List[Tree], List[Tree]) = impl.body.span {
Expand Down Expand Up @@ -1083,8 +1123,11 @@ class Namer { typer: Typer =>

localCtx = completerCtx.inClassContext(selfInfo)

val delayedEntryBuf = collection.mutable.ListBuffer.empty[Context ?=> Context]

index(constr)
index(rest)(using localCtx)
index(rest, delayedEntryBuf)(using localCtx)
delayedEntry = delayedEntryBuf.toList

symbolOfTree(constr).info.stripPoly match // Completes constr symbol as a side effect
case mt: MethodType if cls.is(Case) && mt.isParamDependent =>
Expand Down Expand Up @@ -1189,6 +1232,10 @@ class Namer { typer: Typer =>
denot.info = tempInfo.finalized(parentTypes)
tempInfo = null // The temporary info can now be garbage-collected

// now we know the parents we can decide if we enter the symbols or not
delayedEntry.foldLeft(localCtx) { (localCtx, op) => op(using localCtx) }
delayedEntry = null

Checking.checkWellFormed(cls)
if (isDerivedValueClass(cls)) cls.setFlag(Final)
cls.info = avoidPrivateLeaks(cls)
Expand Down
10 changes: 5 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1941,11 +1941,10 @@ class Typer extends Namer
}

def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = {
if (!sym.info.exists) { // it's a discarded synthetic case class method, drop it
assert(sym.is(Synthetic) && desugar.isRetractableCaseClassMethodName(sym.name))
if !sym.info.exists then // it's a discarded synthetic case class method, or ordinal method drop it
assert(sym.is(Synthetic) && desugar.isRetractableCaseClassOrEnumMethodName(sym.name))
sym.owner.info.decls.openForMutations.unlink(sym)
return EmptyTree
}
val DefDef(name, tparams, vparamss, tpt, _) = ddef
completeAnnotations(ddef, sym)
val tparams1 = tparams.mapconserve(typed(_).asInstanceOf[TypeDef])
Expand Down Expand Up @@ -2105,7 +2104,8 @@ class Typer extends Namer
val constr1 = typed(constr).asInstanceOf[DefDef]
val parentsWithClass = ensureFirstTreeIsClass(parents.mapconserve(typedParent).filterConserve(!_.isEmpty), cdef.nameSpan)
val parents1 = ensureConstrCall(cls, parentsWithClass)(using superCtx)
val firstParent = parents1.head.tpe.dealias.typeSymbol
val firstParentTpe = parents1.head.tpe.dealias
val firstParent = firstParentTpe.typeSymbol

checkEnumParent(cls, firstParent)

Expand All @@ -2122,7 +2122,7 @@ class Typer extends Namer
.withType(dummy.termRef)
if (!cls.isOneOf(AbstractOrTrait) && !ctx.isAfterTyper)
checkRealizableBounds(cls, cdef.sourcePos.withSpan(cdef.nameSpan))
if cls.derivesFrom(defn.EnumClass) then
if cls.isEnum || firstParentTpe.classSymbol.isEnum then
checkEnum(cdef, cls, firstParent)
val cdef1 = assignType(cpy.TypeDef(cdef)(name, impl1), cls)

Expand Down
3 changes: 2 additions & 1 deletion library/src-bootstrapped/scala/Enum.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scala

/** A base trait of all enum classes */
/** A Product that also describes a label and ordinal */
@deprecated("scala.Enum is no longer supported", "3.0.0-M1")
trait Enum extends Product, Serializable:

/** A string uniquely identifying a case of an enum */
Expand Down
3 changes: 3 additions & 0 deletions library/src-non-bootstrapped/scala/Enum.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ package scala
/** A base trait of all enum classes */
trait Enum extends Product, Serializable:

/** A string uniquely identifying a case of an enum */
def enumLabel: String

/** A number uniquely identifying a case of an enum */
def ordinal: Int

This file was deleted.

21 changes: 0 additions & 21 deletions library/src-non-bootstrapped/scala/runtime/EnumValues.scala

This file was deleted.

15 changes: 15 additions & 0 deletions tests/neg/enumsLabel-overrides.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
trait Mixin { def enumLabel: String = "mixin" }

enum Mixed extends Mixin {
case B // error: overriding method enumLabel in trait Mixin of type => String;
}

enum MixedAlso {
case C extends MixedAlso with Mixin // error: overriding method enumLabel in trait Mixin of type => String;
}

trait HasEnumLabel { def enumLabel: String }

enum MyEnum extends HasEnumLabel {
case D // ok
}
15 changes: 15 additions & 0 deletions tests/neg/enumsLabel-singleimpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
enum Labelled {

case A

def enumLabel: String = "nolabel" // error: double definition of method enumLabel: => String

}

enum Ordinalled {

case A

def ordinal: Int = -1 // error: double definition of method ordinal: => Int

}
22 changes: 0 additions & 22 deletions tests/neg/enumsLabelDef.scala

This file was deleted.

Loading