Skip to content

Fix #8001: Erase polymorphic value classes like Scala 2 #10765

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

Merged
merged 3 commits into from
Dec 14, 2020
Merged
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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,7 @@ trait BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
// (one that doesn't erase to the actual signature). See run/t3452b for a test case.

val memberTpe = atPhase(erasurePhase) { moduleClass.denot.thisType.memberInfo(sym) }
val erasedMemberType = TypeErasure.erasure(memberTpe)
val erasedMemberType = TypeErasure.fullErasure(memberTpe)
if (erasedMemberType =:= sym.denot.info)
getGenericSignatureHelper(sym, moduleClass, memberTpe).orNull
else null
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
case TypeErasure.ErasedValueType(tycon1, underlying2) =>
def compareErasedValueType = tp1 match {
case TypeErasure.ErasedValueType(tycon2, underlying1) =>
(tycon1.symbol eq tycon2.symbol) && isSameType(underlying1, underlying2)
(tycon1.symbol eq tycon2.symbol) && isSubType(underlying1, underlying2)
case _ =>
secondTry
}
Expand Down
78 changes: 51 additions & 27 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import scala.annotation.tailrec
object TypeErasure {

private def erasureDependsOnArgs(sym: Symbol)(using Context) =
sym == defn.ArrayClass || sym == defn.PairClass
sym == defn.ArrayClass || sym == defn.PairClass || isDerivedValueClass(sym)

def normalizeClass(cls: ClassSymbol)(using Context): ClassSymbol = {
if (cls.owner == defn.ScalaPackageClass) {
Expand All @@ -59,7 +59,7 @@ object TypeErasure {
case tp: TypeRef =>
val sym = tp.symbol
sym.isClass &&
!erasureDependsOnArgs(sym) &&
(!erasureDependsOnArgs(sym) || isDerivedValueClass(sym)) &&
!defn.specialErasure.contains(sym) &&
!defn.isSyntheticFunctionClass(sym)
case _: TermRef =>
Expand Down Expand Up @@ -157,7 +157,7 @@ object TypeErasure {

def sigName(tp: Type, isJava: Boolean)(using Context): TypeName = {
val normTp = tp.translateFromRepeated(toArray = isJava)
val erase = erasureFn(isJava, semiEraseVCs = false, isConstructor = false, wildcardOK = true)
val erase = erasureFn(isJava, semiEraseVCs = true, isConstructor = false, wildcardOK = true)
erase.sigName(normTp)(using preErasureCtx)
}

Expand Down Expand Up @@ -444,14 +444,15 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
case tp: TypeRef =>
val sym = tp.symbol
if (!sym.isClass) this(tp.translucentSuperType)
else if (semiEraseVCs && isDerivedValueClass(sym)) eraseDerivedValueClassRef(tp)
else if (semiEraseVCs && isDerivedValueClass(sym)) eraseDerivedValueClass(tp)
else if (defn.isSyntheticFunctionClass(sym)) defn.erasedFunctionType(sym)
else eraseNormalClassRef(tp)
case tp: AppliedType =>
val tycon = tp.tycon
if (tycon.isRef(defn.ArrayClass)) eraseArray(tp)
else if (tycon.isRef(defn.PairClass)) erasePair(tp)
else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = isJava))
else if (semiEraseVCs && isDerivedValueClass(tycon.classSymbol)) eraseDerivedValueClass(tp)
else apply(tp.translucentSuperType)
case _: TermRef | _: ThisType =>
this(tp.widen)
Expand Down Expand Up @@ -551,12 +552,34 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
case rt => MethodType(Nil, Nil, rt)
case tp1 => this(tp1)

private def eraseDerivedValueClassRef(tref: TypeRef)(using Context): Type = {
val cls = tref.symbol.asClass
val underlying = underlyingOfValueClass(cls)
if underlying.exists && !isCyclic(cls) then
val erasedValue = valueErasure(underlying)
if erasedValue.exists then ErasedValueType(tref, erasedValue)
private def eraseDerivedValueClass(tp: Type)(using Context): Type = {
val cls = tp.classSymbol.asClass
val unbox = valueClassUnbox(cls)
if unbox.exists then
val genericUnderlying = unbox.info.resultType
val underlying = tp.select(unbox).widen.resultType

val erasedUnderlying = erasure(underlying)

// Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to
// be binary-compatible with Scala 2 we need two special cases for polymorphic
// value classes:
// - Given `class Foo[A](x: A) extends AnyVal`, `Foo[X]` should erase like
// `X`, except if its a primitive in which case it erases to the boxed
// version of this primitive.
// - Given `class Bar[A](x: Array[A]) extends AnyVal`, `Bar[X]` will be
// erased like `Array[A]` as seen from its definition site, no matter
// the `X` (same if `A` is bounded).
//
// The binary compatibility is checked by sbt-dotty/sbt-test/scala2-compat/i8001
val erasedValueClass =
if erasedUnderlying.isPrimitiveValueType && !genericUnderlying.isPrimitiveValueType then
defn.boxedType(erasedUnderlying)
else if genericUnderlying.derivesFrom(defn.ArrayClass) then
erasure(genericUnderlying)
else erasedUnderlying

if erasedValueClass.exists then ErasedValueType(cls.typeRef, erasedValueClass)
else
assert(ctx.reporter.errorsReported, i"no erasure for $underlying")
NoType
Expand All @@ -569,22 +592,23 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
}

/** The erasure of a function result type. */
private def eraseResult(tp: Type)(using Context): Type = tp match {
case tp: TypeRef =>
val sym = tp.symbol
if (sym eq defn.UnitClass) sym.typeRef
// For a value class V, "new V(x)" should have type V for type adaptation to work
// correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the return type of a
// constructor method should not be semi-erased.
else if (isConstructor && isDerivedValueClass(sym)) eraseNormalClassRef(tp)
else this(tp)
case tp: AppliedType =>
val sym = tp.tycon.typeSymbol
if (sym.isClass && !erasureDependsOnArgs(sym)) eraseResult(tp.tycon)
else this(tp)
case _ =>
this(tp)
}
def eraseResult(tp: Type)(using Context): Type =
// For a value class V, "new V(x)" should have type V for type adaptation to work
// correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a
// constructor method should not be semi-erased.
if semiEraseVCs && isConstructor && !tp.isInstanceOf[MethodOrPoly] then
erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp)
else tp match
case tp: TypeRef =>
val sym = tp.symbol
if (sym eq defn.UnitClass) sym.typeRef
else this(tp)
case tp: AppliedType =>
val sym = tp.tycon.typeSymbol
if (sym.isClass && !erasureDependsOnArgs(sym)) eraseResult(tp.tycon)
else this(tp)
case _ =>
this(tp)

/** The name of the type as it is used in `Signature`s.
* Need to ensure correspondence with erasure!
Expand All @@ -602,7 +626,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
return sigName(info)
}
if (isDerivedValueClass(sym)) {
val erasedVCRef = eraseDerivedValueClassRef(tp)
val erasedVCRef = eraseDerivedValueClass(tp)
if (erasedVCRef.exists) return sigName(erasedVCRef)
}
if (defn.isSyntheticFunctionClass(sym))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
ValueClassesMayNotContainInitalizationID,
ValueClassesMayNotBeAbstractID,
ValueClassesMayNotBeContaintedID,
ValueClassesMayNotWrapItselfID,
ValueClassesMayNotWrapAnotherValueClassID,
ValueClassParameterMayNotBeAVarID,
ValueClassNeedsExactlyOneValParamID,
OnlyCaseClassOrCaseObjectAllowedID,
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1617,9 +1617,9 @@ import transform.SymUtils._
def explain = ""
}

class ValueClassesMayNotWrapItself(valueClass: Symbol)(using Context)
extends SyntaxMsg(ValueClassesMayNotWrapItselfID) {
def msg = """A value class may not wrap itself"""
class ValueClassesMayNotWrapAnotherValueClass(valueClass: Symbol)(using Context)
extends SyntaxMsg(ValueClassesMayNotWrapAnotherValueClassID) {
def msg = """A value class may not wrap another user-defined value class"""
def explain = ""
}

Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ object Erasure {
* in ExtensionMethods#transform.
*/
def cast(tree: Tree, pt: Type)(using Context): Tree = trace(i"cast ${tree.tpe.widen} --> $pt", show = true) {

def wrap(tycon: TypeRef) =
ref(u2evt(tycon.typeSymbol.asClass)).appliedTo(tree)
def unwrap(tycon: TypeRef) =
Expand Down Expand Up @@ -357,7 +356,10 @@ object Erasure {
if (pt.isInstanceOf[ProtoType] || tree.tpe <:< pt)
tree
else if (tpw.isErasedValueType)
adaptToType(box(tree), pt)
if (pt.isErasedValueType) then
tree.asInstance(pt)
else
adaptToType(box(tree), pt)
else if (pt.isErasedValueType)
adaptToType(unbox(tree, pt), pt)
else if (tpw.isPrimitiveValueType && !pt.isPrimitiveValueType)
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/FirstTransform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import NameOps._
import NameKinds.OuterSelectName
import StdNames._
import NullOpsDecorator._
import TypeUtils.isErasedValueType

object FirstTransform {
val name: String = "firstTransform"
Expand Down Expand Up @@ -67,7 +68,7 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase =>
qual.tpe
}
assert(
qualTpe.derivesFrom(tree.symbol.owner) ||
qualTpe.isErasedValueType || qualTpe.derivesFrom(tree.symbol.owner) ||
tree.symbol.is(JavaStatic) && qualTpe.derivesFrom(tree.symbol.enclosingClass),
i"non member selection of ${tree.symbol.showLocated} from ${qualTpe} in $tree")
case _: TypeTree =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,11 @@ object GenericSignatures {
else if (sym == defn.UnitClass) jsig(defn.BoxedUnitClass.typeRef)
else builder.append(defn.typeTag(sym.info))
else if (ValueClasses.isDerivedValueClass(sym)) {
val unboxed = ValueClasses.valueClassUnbox(sym.asClass).info.finalResultType
val unboxedSeen = tp.memberInfo(ValueClasses.valueClassUnbox(sym.asClass)).finalResultType
if (unboxedSeen.isPrimitiveValueType && !primitiveOK)
val erasedUnderlying = core.TypeErasure.fullErasure(tp)
if (erasedUnderlying.isPrimitiveValueType && !primitiveOK)
classSig(sym, pre, args)
else
jsig(unboxedSeen, toplevel, primitiveOK)
jsig(erasedUnderlying, toplevel, primitiveOK)
}
else if (defn.isSyntheticFunctionClass(sym)) {
val erasedSym = defn.erasedFunctionClass(sym)
Expand Down
13 changes: 1 addition & 12 deletions compiler/src/dotty/tools/dotc/transform/ValueClasses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SymUtils._
/** Methods that apply to user-defined value classes */
object ValueClasses {

def isDerivedValueClass(sym: Symbol)(using Context): Boolean = {
def isDerivedValueClass(sym: Symbol)(using Context): Boolean = sym.isClass && {
val d = sym.denot
!d.isRefinementClass &&
d.isValueClass &&
Expand Down Expand Up @@ -54,15 +54,4 @@ object ValueClasses {
/** The unboxed type that underlies a derived value class */
def underlyingOfValueClass(sym: ClassSymbol)(using Context): Type =
valueClassUnbox(sym).info.resultType

/** Whether a value class wraps itself */
def isCyclic(cls: ClassSymbol)(using Context): Boolean = {
def recur(seen: Set[Symbol], clazz: ClassSymbol)(using Context): Boolean =
(seen contains clazz) || {
val unboxed = underlyingOfValueClass(clazz).typeSymbol
(isDerivedValueClass(unboxed)) && recur(seen + clazz, unboxed.asClass)
}

recur(Set[Symbol](), cls)
}
}
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,8 @@ object Checking {
report.error(ValueClassesMayNotBeAbstract(clazz), clazz.srcPos)
if (!clazz.isStatic)
report.error(ValueClassesMayNotBeContainted(clazz), clazz.srcPos)
if (isCyclic(clazz.asClass))
report.error(ValueClassesMayNotWrapItself(clazz), clazz.srcPos)
if (isDerivedValueClass(underlyingOfValueClass(clazz.asClass).classSymbol))
report.error(ValueClassesMayNotWrapAnotherValueClass(clazz), clazz.srcPos)
else {
val clParamAccessors = clazz.asClass.paramAccessors.filter { param =>
param.isTerm && !param.is(Flags.Accessor)
Expand Down
24 changes: 0 additions & 24 deletions compiler/test/dotty/tools/dotc/MissingCoreLibTests.scala

This file was deleted.

15 changes: 15 additions & 0 deletions sbt-dotty/sbt-test/scala2-compat/i8001/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
val scala3Version = sys.props("plugin.scalaVersion")
val scala2Version = "2.13.4"

lazy val lib = (project in file ("lib"))
.settings(scalaVersion := scala2Version)

lazy val test = (project in file ("main"))
.dependsOn(lib)
.settings(
scalaVersion := scala3Version,
// https://github.com/sbt/sbt/issues/5369
projectDependencies := {
projectDependencies.value.map(_.withDottyCompat(scalaVersion.value))
}
)
43 changes: 43 additions & 0 deletions sbt-dotty/sbt-test/scala2-compat/i8001/lib/lib.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Poly[A](val value: A) extends AnyVal

class Arr[A](val value: Array[A]) extends AnyVal

class ArrRef[A <: AnyRef](val value: Array[A]) extends AnyVal

class A {
def poly1(x: Poly[Int]): Poly[Int] =
new Poly(x.value)

def poly2(x: Poly[String]): Poly[String] =
new Poly(x.value)

def poly3(x: Poly[Array[Int]]): Poly[Array[Int]] =
new Poly(x.value)

def poly4(x: Poly[Array[String]]): Poly[Array[String]] =
new Poly(x.value)

def arr1(x: Arr[Int]): Arr[Int] =
new Arr(x.value)

def arr2(x: Arr[String]): Arr[String] =
new Arr(x.value)

def arr3(x: Arr[Array[Int]]): Arr[Array[Int]] =
new Arr(x.value)

def arr4(x: Arr[Array[String]]): Arr[Array[String]] =
new Arr(x.value)

def arrRef1(x: ArrRef[Integer]): ArrRef[Integer] =
new ArrRef(x.value)

def arrRef2(x: ArrRef[String]): ArrRef[String] =
new ArrRef(x.value)

def arrRef3(x: ArrRef[Array[Int]]): ArrRef[Array[Int]] =
new ArrRef(x.value)

def arrRef4(x: ArrRef[Array[String]]): ArrRef[Array[String]] =
new ArrRef(x.value)
}
20 changes: 20 additions & 0 deletions sbt-dotty/sbt-test/scala2-compat/i8001/main/test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
object B {
def main(args: Array[String]): Unit = {
val a = new A

a.poly1(new Poly(1))
a.poly2(new Poly(""))
a.poly3(new Poly(Array(1)))
a.poly4(new Poly(Array("")))

a.arr1(new Arr(Array(1)))
a.arr2(new Arr(Array("")))
a.arr3(new Arr(Array(Array(1))))
a.arr4(new Arr(Array(Array(""))))

a.arrRef1(new ArrRef(Array(1)))
a.arrRef2(new ArrRef(Array("")))
a.arrRef3(new ArrRef(Array(Array(1))))
a.arrRef4(new ArrRef(Array(Array(""))))
}
}
1 change: 1 addition & 0 deletions sbt-dotty/sbt-test/scala2-compat/i8001/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version"))
1 change: 1 addition & 0 deletions sbt-dotty/sbt-test/scala2-compat/i8001/test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
> test/run
File renamed without changes.
5 changes: 4 additions & 1 deletion tests/neg/i1642.scala
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
class Test2(val valueVal: Test2) extends AnyVal // error: value class cannot wrap itself
class Test0(val valueVal: Test0) extends AnyVal // error: value class cannot wrap itself

class Test1(val x: Int) extends AnyVal
class Test2(val y: Test1) extends AnyVal // error: value class may not wrap another user-defined value class
2 changes: 1 addition & 1 deletion tests/neg/i5005.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

case class i0 (i0: i1) extends AnyVal
case class i0 (i0: i1) extends AnyVal // error
trait i1 extends i0 // error

trait F[x] extends AnyVal // error
Expand Down
2 changes: 0 additions & 2 deletions tests/pos/i1642.scala

This file was deleted.

Loading