From c08543a1acd30ffa1fc2d127f18c831734148a76 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 4 Mar 2021 15:49:12 +0100 Subject: [PATCH 1/5] Scala2Unpickler: Support structural member selection Structural members need to be selected by name, not by symbols. Also deleted a special case in finishSym that was dead code: refinement classes are classes so the `owner.isClass` branch was always taken. --- .../dotc/core/unpickleScala2/Scala2Unpickler.scala | 11 +++++++---- sbt-dotty/sbt-test/scala2-compat/structural/build.sbt | 9 +++++++++ .../sbt-test/scala2-compat/structural/lib/lib.scala | 10 ++++++++++ .../sbt-test/scala2-compat/structural/main/test.scala | 4 ++++ .../scala2-compat/structural/project/plugins.sbt | 1 + sbt-dotty/sbt-test/scala2-compat/structural/test | 1 + 6 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 sbt-dotty/sbt-test/scala2-compat/structural/build.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/structural/lib/lib.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/structural/main/test.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/structural/project/plugins.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/structural/test diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index f77dc285ee8e..a38c2a12fd0a 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -491,8 +491,6 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas val owner = sym.owner if (owner.isClass) owner.asClass.enter(sym, symScope(owner)) - else if (isRefinementClass(owner)) - symScope(owner).openForMutations.enter(sym) } sym } @@ -727,6 +725,11 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas * (if restpe is not a ClassInfoType, a MethodType or a NullaryMethodType, which leaves TypeRef/SingletonType -- the latter would make the polytype a type constructor) */ protected def readType()(using Context): Type = { + def select(pre: Type, sym: Symbol): Type = + // structural members need to be selected by name, their symbols are only + // valid in the synthetic refinement class that defines them. + if !pre.isInstanceOf[ThisType] && isRefinementClass(sym.owner) then pre.select(sym.name) else pre.select(sym) + val tag = readByte() val end = readNat() + readIndex (tag: @switch) match { @@ -739,7 +742,7 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas case SINGLEtpe => val pre = readPrefix() val sym = readDisambiguatedSymbolRef(_.info.isParameterless) - pre.select(sym) + select(pre, sym) case SUPERtpe => val thistpe = readTypeRef() val supertpe = readTypeRef() @@ -770,7 +773,7 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas pre = sym.owner.thisType case _ => } - val tycon = pre.select(sym) + val tycon = select(pre, sym) val args = until(end, () => readTypeRef()) if (sym == defn.ByNameParamClass2x) ExprType(args.head) else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) diff --git a/sbt-dotty/sbt-test/scala2-compat/structural/build.sbt b/sbt-dotty/sbt-test/scala2-compat/structural/build.sbt new file mode 100644 index 000000000000..61d8f34cd5f6 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/structural/build.sbt @@ -0,0 +1,9 @@ +val scala3Version = sys.props("plugin.scalaVersion") +val scala2Version = "2.13.5" + +lazy val lib = (project in file ("lib")) + .settings(scalaVersion := scala2Version) + +lazy val test = (project in file ("main")) + .dependsOn(lib) + .settings(scalaVersion := scala3Version) diff --git a/sbt-dotty/sbt-test/scala2-compat/structural/lib/lib.scala b/sbt-dotty/sbt-test/scala2-compat/structural/lib/lib.scala new file mode 100644 index 000000000000..7dadd67ab546 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/structural/lib/lib.scala @@ -0,0 +1,10 @@ +class D +object Scala2 { + val structural1: { type DSub <: D; val member: Int } = new { type DSub <: D; val member: Int = 1 } + val dsub: structural1.DSub = null.asInstanceOf[structural1.DSub] + val mbr: structural1.member.type = structural1.member + + def a(a: structural1.DSub): Unit = {} + + def b(a: structural1.member.type): Unit = {} +} diff --git a/sbt-dotty/sbt-test/scala2-compat/structural/main/test.scala b/sbt-dotty/sbt-test/scala2-compat/structural/main/test.scala new file mode 100644 index 000000000000..ac108f64c425 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/structural/main/test.scala @@ -0,0 +1,4 @@ +object Test extends App { + Scala2.a(Scala2.dsub) + Scala2.b(Scala2.mbr) +} diff --git a/sbt-dotty/sbt-test/scala2-compat/structural/project/plugins.sbt b/sbt-dotty/sbt-test/scala2-compat/structural/project/plugins.sbt new file mode 100644 index 000000000000..c17caab2d98c --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/structural/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version")) diff --git a/sbt-dotty/sbt-test/scala2-compat/structural/test b/sbt-dotty/sbt-test/scala2-compat/structural/test new file mode 100644 index 000000000000..8aedcd64cf5b --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/structural/test @@ -0,0 +1 @@ +> test/run From 295d598611fa04cf4b469dca6dcc156ea3047b9d Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Sat, 9 May 2020 18:22:23 +0200 Subject: [PATCH 2/5] Add a Scala 2 erasure mode Previously we only distinguished between Java and Scala erasure, now we have Java, Scala 2 and Scala 3 erasure modes. This commit only adds the machinery needed to make that distinction, latter commits in this PR will introduce differences between the Scala 2 and 3 modes. --- .../dotty/tools/dotc/core/Denotations.scala | 41 +++---- .../src/dotty/tools/dotc/core/Signature.scala | 8 +- .../dotty/tools/dotc/core/TypeErasure.scala | 115 ++++++++++++------ .../src/dotty/tools/dotc/core/Types.scala | 40 +++--- .../dotc/transform/SyntheticMembers.scala | 4 +- 5 files changed, 125 insertions(+), 83 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index 9b7893925e29..f448f26d6f26 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -582,8 +582,7 @@ object Denotations { */ def prefix: Type = NoPrefix - /** Either the Scala or Java signature of the info, depending on where the - * symbol is defined. + /** The symbol-specific signature of the info. * * Invariants: * - Before erasure, the signature of a denotation is always equal to the @@ -595,17 +594,17 @@ object Denotations { * SingleDenotations will have distinct signatures (cf #9050). */ final def signature(using Context): Signature = - signature(isJava = !isType && symbol.is(JavaDefined)) + signature(sourceLanguage = if isType then SourceLanguage.Scala3 else SourceLanguage(symbol)) - /** Overload of `signature` which lets the caller pick between the Java and - * Scala signature of the info. Useful to match denotations defined in + /** Overload of `signature` which lets the caller pick the language used + * to compute the signature of the info. Useful to match denotations defined in * different classes (see `matchesLoosely`). */ - def signature(isJava: Boolean)(using Context): Signature = + def signature(sourceLanguage: SourceLanguage)(using Context): Signature = if (isType) Signature.NotAMethod // don't force info if this is a type denotation else info match { case info: MethodOrPoly => - try info.signature(isJava) + try info.signature(sourceLanguage) catch { // !!! DEBUG case scala.util.control.NonFatal(ex) => report.echo(s"cannot take signature of $info") @@ -1013,36 +1012,36 @@ object Denotations { /** `matches` without a target name check. * - * We consider a Scala method and a Java method to match if they have - * matching Scala signatures. This allows us to override some Java - * definitions even if they have a different erasure (see i8615b, - * i9109b), Erasure takes care of adding any necessary bridge to make - * this work at runtime. + * For definitions coming from different languages, we pick a common + * language to compute their signatures. This allows us for example to + * override some Java definitions from Scala even if they have a different + * erasure (see i8615b, i9109b), Erasure takes care of adding any necessary + * bridge to make this work at runtime. */ def matchesLoosely(other: SingleDenotation)(using Context): Boolean = if isType then true else - val isJava = symbol.is(JavaDefined) - val otherIsJava = other.symbol.is(JavaDefined) - val useJavaSig = isJava && otherIsJava - val sig = signature(isJava = useJavaSig) - val otherSig = other.signature(isJava = useJavaSig) + val thisLanguage = SourceLanguage(symbol) + val otherLanguage = SourceLanguage(other.symbol) + val commonLanguage = SourceLanguage.commonLanguage(thisLanguage, otherLanguage) + val sig = signature(commonLanguage) + val otherSig = other.signature(commonLanguage) sig.matchDegree(otherSig) match case FullMatch => true case MethodNotAMethodMatch => !ctx.erasedTypes && { // A Scala zero-parameter method and a Scala non-method always match. - if !isJava && !otherIsJava then + if !thisLanguage.isJava && !otherLanguage.isJava then true // Java allows defining both a field and a zero-parameter method with the same name, // so they must not match. - else if isJava && otherIsJava then + else if thisLanguage.isJava && otherLanguage.isJava then false // A Java field never matches a Scala method. - else if isJava then + else if thisLanguage.isJava then symbol.is(Method) - else // otherIsJava + else // otherLanguage.isJava other.symbol.is(Method) } case ParamMatch => diff --git a/compiler/src/dotty/tools/dotc/core/Signature.scala b/compiler/src/dotty/tools/dotc/core/Signature.scala index 4636c13dde66..d9b5dcca81e3 100644 --- a/compiler/src/dotty/tools/dotc/core/Signature.scala +++ b/compiler/src/dotty/tools/dotc/core/Signature.scala @@ -109,8 +109,8 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) { * * Like Signature#apply, the result is only cacheable if `isUnderDefined == false`. */ - def prependTermParams(params: List[Type], isJava: Boolean)(using Context): Signature = - Signature(params.map(p => sigName(p, isJava)) ::: paramsSig, resSig) + def prependTermParams(params: List[Type], sourceLanguage: SourceLanguage)(using Context): Signature = + Signature(params.map(p => sigName(p, sourceLanguage)) ::: paramsSig, resSig) /** Construct a signature by prepending the length of a type parameter section * to the parameter part of this signature. @@ -164,9 +164,9 @@ object Signature { * otherwise the signature will change once the contained type variables have * been instantiated. */ - def apply(resultType: Type, isJava: Boolean)(using Context): Signature = { + def apply(resultType: Type, sourceLanguage: SourceLanguage)(using Context): Signature = { assert(!resultType.isInstanceOf[ExprType]) - apply(Nil, sigName(resultType, isJava)) + apply(Nil, sigName(resultType, sourceLanguage)) } val lexicographicOrdering: Ordering[Signature] = new Ordering[Signature] { diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 31c6982696cc..56f9dee631d8 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -14,6 +14,40 @@ import Decorators._ import Definitions.MaxImplementedFunctionArity import scala.annotation.tailrec +/** The language in which the definition being erased was written. */ +enum SourceLanguage: + case Java, Scala2, Scala3 + def isJava: Boolean = this eq Java + def isScala2: Boolean = this eq Scala2 + def isScala3: Boolean = this eq Scala3 +object SourceLanguage: + /** The language in which `sym` was defined. */ + def apply(sym: Symbol)(using Context): SourceLanguage = + if sym.is(JavaDefined) then + SourceLanguage.Java + // Scala 2 methods don't have Inline set, except for the ones injected with `patchStdlibClass` + // which are really Scala 3 methods. + else if sym.isClass && sym.is(Scala2x) || (sym.maybeOwner.is(Scala2x) && !sym.is(Inline)) then + SourceLanguage.Scala2 + else + SourceLanguage.Scala3 + + /** Number of bits needed to represent this enum. */ + def bits: Int = + val len = values.length + val log2 = 31 - Integer.numberOfLeadingZeros(len) + if len == 1 << log2 then + log2 + else + log2 + 1 + + /** A common language to use when matching definitions written in different + * languages. + */ + def commonLanguage(x: SourceLanguage, y: SourceLanguage): SourceLanguage = + if x.ordinal > y.ordinal then x else y +end SourceLanguage + /** Erased types are: * * ErasedValueType @@ -107,28 +141,29 @@ object TypeErasure { } } - private def erasureIdx(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) = - (if (isJava) 1 else 0) + - (if (semiEraseVCs) 2 else 0) + - (if (isConstructor) 4 else 0) + - (if (wildcardOK) 8 else 0) + private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) = + extension (b: Boolean) def toInt = if b then 1 else 0 + wildcardOK.toInt + + (isConstructor.toInt << 1) + + (semiEraseVCs.toInt << 2) + + (sourceLanguage.ordinal << 3) - private val erasures = new Array[TypeErasure](16) + private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 3)) - for { - isJava <- List(false, true) + for + sourceLanguage <- SourceLanguage.values semiEraseVCs <- List(false, true) isConstructor <- List(false, true) wildcardOK <- List(false, true) - } - erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK)) = - new TypeErasure(isJava, semiEraseVCs, isConstructor, wildcardOK) + do + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) = + new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK) /** Produces an erasure function. See the documentation of the class [[TypeErasure]] * for a description of each parameter. */ - private def erasureFn(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure = - erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK)) + private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure = + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) /** The current context with a phase no later than erasure */ def preErasureCtx(using Context) = @@ -139,7 +174,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(isJava = false, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -147,7 +182,7 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(isJava = false, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -155,9 +190,9 @@ object TypeErasure { case ErasedValueType(_, underlying) => erasure(underlying) case etp => etp - def sigName(tp: Type, isJava: Boolean)(using Context): TypeName = { - val normTp = tp.translateFromRepeated(toArray = isJava) - val erase = erasureFn(isJava, semiEraseVCs = true, isConstructor = false, wildcardOK = true) + def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = { + val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava) + val erase = erasureFn(sourceLanguage, semiEraseVCs = true, isConstructor = false, wildcardOK = true) erase.sigName(normTp)(using preErasureCtx) } @@ -181,15 +216,13 @@ object TypeErasure { * - For $asInstanceOf : [T]T * - For $isInstanceOf : [T]Boolean * - For all abstract types : = ? - * - For Java-defined symbols: : the erasure of their type with isJava = true, - * semiEraseVCs = false. Semi-erasure never happens in Java. - * - For all other symbols : the semi-erasure of their types, with - * isJava, isConstructor set according to symbol. + * + * `sourceLanguage`, `isConstructor` and `semiEraseVCs` are set based on the symbol. */ def transformInfo(sym: Symbol, tp: Type)(using Context): Type = { - val isJava = sym is JavaDefined - val semiEraseVCs = !isJava - val erase = erasureFn(isJava, semiEraseVCs, sym.isConstructor, wildcardOK = false) + val sourceLanguage = SourceLanguage(sym) + val semiEraseVCs = !sourceLanguage.isJava // Java sees our value classes as regular classes. + val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, wildcardOK = false) def eraseParamBounds(tp: PolyType): Type = tp.derivedLambdaType( @@ -391,18 +424,20 @@ object TypeErasure { case _ => false } } + import TypeErasure._ /** - * @param isJava Arguments should be treated the way Java does it - * @param semiEraseVCs If true, value classes are semi-erased to ErasedValueType - * (they will be fully erased in [[ElimErasedValueType]]). - * If false, they are erased like normal classes. - * @param isConstructor Argument forms part of the type of a constructor - * @param wildcardOK Wildcards are acceptable (true when using the erasure - * for computing a signature name). + * @param sourceLanguage Adapt our erasure rules to mimic what the given language + * would do. + * @param semiEraseVCs If true, value classes are semi-erased to ErasedValueType + * (they will be fully erased in [[ElimErasedValueType]]). + * If false, they are erased like normal classes. + * @param isConstructor Argument forms part of the type of a constructor + * @param wildcardOK Wildcards are acceptable (true when using the erasure + * for computing a signature name). */ -class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) { +class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) { /** The erasure |T| of a type T. This is: * @@ -450,7 +485,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean 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 (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava)) else if (semiEraseVCs && isDerivedValueClass(tycon.classSymbol)) eraseDerivedValueClass(tp) else apply(tp.translucentSuperType) case _: TermRef | _: ThisType => @@ -468,12 +503,12 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean case tp: TypeProxy => this(tp.underlying) case AndType(tp1, tp2) => - erasedGlb(this(tp1), this(tp2), isJava) + erasedGlb(this(tp1), this(tp2), sourceLanguage.isJava) case OrType(tp1, tp2) => TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase) + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)(tpToErase) val (names, formals0) = if (tp.isErasedMethod) (Nil, Nil) else (tp.paramNames, tp.paramInfos) val formals = formals0.mapConserve(paramErasure) eraseResult(tp.resultType) match { @@ -516,8 +551,8 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean private def eraseArray(tp: Type)(using Context) = { val defn.ArrayOf(elemtp) = tp if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType) - else if (isUnboundedGeneric(elemtp) && !isJava) defn.ObjectType - else JavaArrayType(erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) + else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType + else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp)) } private def erasePair(tp: Type)(using Context): Type = { @@ -544,7 +579,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean // See doc comment for ElimByName for speculation how we could improve this. else MethodType(Nil, Nil, - eraseResult(sym.info.finalResultType.translateFromRepeated(toArray = isJava))) + eraseResult(sym.info.finalResultType.translateFromRepeated(toArray = sourceLanguage.isJava))) case tp1: PolyType => eraseResult(tp1.resultType) match case rt: MethodType => rt @@ -596,7 +631,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean // 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) + erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp) else tp match case tp: TypeRef => val sym = tp.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d3c710033c29..cfd441f36bc6 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3345,6 +3345,8 @@ object Types { private var mySignatureRunId: Int = NoRunId private var myJavaSignature: Signature = _ private var myJavaSignatureRunId: Int = NoRunId + private var myScala2Signature: Signature = _ + private var myScala2SignatureRunId: Int = NoRunId /** If `isJava` is false, the Scala signature of this method. Otherwise, its Java signature. * @@ -3360,31 +3362,37 @@ object Types { * * @see SingleDenotation#signature */ - def signature(isJava: Boolean)(using Context): Signature = - def computeSignature(isJava: Boolean)(using Context): Signature = + def signature(sourceLanguage: SourceLanguage)(using Context): Signature = + def computeSignature(using Context): Signature = val resultSignature = resultType match - case tp: MethodOrPoly => tp.signature(isJava) + case tp: MethodOrPoly => tp.signature(sourceLanguage) case tp: ExprType => tp.signature case tp => if tp.isRef(defn.UnitClass) then Signature(Nil, defn.UnitClass.fullName.asTypeName) - else Signature(tp, isJava) + else Signature(tp, sourceLanguage) this match case tp: MethodType => val params = if (isErasedMethod) Nil else tp.paramInfos - resultSignature.prependTermParams(params, isJava) + resultSignature.prependTermParams(params, sourceLanguage) case tp: PolyType => resultSignature.prependTypeParams(tp.paramNames.length) - if isJava then - if ctx.runId != myJavaSignatureRunId then - myJavaSignature = computeSignature(isJava) - if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId - myJavaSignature - else - if ctx.runId != mySignatureRunId then - mySignature = computeSignature(isJava) - if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId - mySignature + sourceLanguage match + case SourceLanguage.Java => + if ctx.runId != myJavaSignatureRunId then + myJavaSignature = computeSignature + if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId + myJavaSignature + case SourceLanguage.Scala2 => + if ctx.runId != myScala2SignatureRunId then + myScala2Signature = computeSignature + if !myScala2Signature.isUnderDefined then myScala2SignatureRunId = ctx.runId + myScala2Signature + case SourceLanguage.Scala3 => + if ctx.runId != mySignatureRunId then + mySignature = computeSignature + if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId + mySignature end signature /** The Scala signature of this method. Note that two distinct Java method @@ -3392,7 +3400,7 @@ object Types { * `signature` can be used to avoid ambiguity if necessary. */ final override def signature(using Context): Signature = - signature(isJava = false) + signature(sourceLanguage = SourceLanguage.Scala3) final override def hashCode: Int = System.identityHashCode(this) diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 4b28bdb18cc9..36a9812b9864 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -385,12 +385,12 @@ class SyntheticMembers(thisPhase: DenotTransformer) { private def hasWriteReplace(clazz: ClassSymbol)(using Context): Boolean = clazz.membersNamed(nme.writeReplace) - .filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, isJava = false)) + .filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, sourceLanguage = SourceLanguage.Scala3)) .exists private def hasReadResolve(clazz: ClassSymbol)(using Context): Boolean = clazz.membersNamed(nme.readResolve) - .filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, isJava = false)) + .filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, sourceLanguage = SourceLanguage.Scala3)) .exists private def writeReplaceDef(clazz: ClassSymbol)(using Context): TermSymbol = From 5337ac5990ed62f179af2db995ce3e1ff73975aa Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 4 Mar 2021 17:28:38 +0100 Subject: [PATCH 3/5] SingleDenotation#signature: Use Scala 3 signature for non-SymDenotations For SymDenotations, it's important that we use a signature that matches how the type is erased to maintain the invariant that two overloads in the same owner have different signatures, but for non-SymDenotations we don't make this guarantee so we can choose whatever we want as long as it's consistent. I originally chose to treat them like SymDenotation for consistency, but we're about to add some complex (and therefore somewhat expensive) logic for dealing with Scala 2 intersection erasure, by using the Scala 3 signature for all non-SymDenotation we avoid calling this logic more than we need to. --- compiler/src/dotty/tools/dotc/core/Denotations.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index f448f26d6f26..5d4675dc207f 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -582,7 +582,9 @@ object Denotations { */ def prefix: Type = NoPrefix - /** The symbol-specific signature of the info. + /** For SymDenotations, the language-specific signature of the info, depending on + * where the symbol is defined. For non-SymDenotations, the Scala 3 + * signature. * * Invariants: * - Before erasure, the signature of a denotation is always equal to the @@ -594,7 +596,7 @@ object Denotations { * SingleDenotations will have distinct signatures (cf #9050). */ final def signature(using Context): Signature = - signature(sourceLanguage = if isType then SourceLanguage.Scala3 else SourceLanguage(symbol)) + signature(sourceLanguage = if isType || !this.isInstanceOf[SymDenotation] then SourceLanguage.Scala3 else SourceLanguage(symbol)) /** Overload of `signature` which lets the caller pick the language used * to compute the signature of the info. Useful to match denotations defined in From e4af70b764f52c8b2128753b81c8ac2cd177ba61 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 17 Feb 2021 17:57:20 +0100 Subject: [PATCH 4/5] Correctly erase Scala 2 intersection types Because our algorithm for erasing intersection types does not exactly match the one used by Scala 2, we could end up emitting calls to Scala 2 methods with the wrong bytecode signature, leading to NoSuchMethodError at runtime. We could try to exactly match what Scala 2 does, but it turns out that the Scala 2 logic heavily relies on implementation details which makes it extremely complex to reliably replicate. Therefore, this commit instead special-cases the erasure of Scala 2 intersections (just like we already special-case the erasure of Java intersections) and limits which Scala 2 intersection types we support to a subset that we can erase without too much complications (but even that still requires ~200 lines of code!). This means that we're now free to change the way we erase intersections in any way we want without introducing more compatibility problems (until 3.0.0 that is), I'll explore this in a follow-up PR. Fixes #4619. Fixes #9175. --- .../dotty/tools/dotc/core/TypeErasure.scala | 12 +- .../core/unpickleScala2/Scala2Erasure.scala | 258 ++++++++++++++++++ .../sbt-test/scala2-compat/erasure/build.sbt | 15 + .../scala2-compat/erasure/changes/Main.scala | 18 ++ .../scala2-compat/erasure/dottyApp/Api.scala | 149 ++++++++++ .../scala2-compat/erasure/dottyApp/Main.scala | 82 ++++++ .../scala2-compat/erasure/project/plugins.sbt | 1 + .../scala2-compat/erasure/scala2Lib/Api.scala | 149 ++++++++++ sbt-dotty/sbt-test/scala2-compat/erasure/test | 3 + tests/run/array-erasure.scala | 45 +++ tests/run/i9175.scala | 10 + 11 files changed, 740 insertions(+), 2 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Erasure.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/project/plugins.sbt create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala create mode 100644 sbt-dotty/sbt-test/scala2-compat/erasure/test create mode 100644 tests/run/array-erasure.scala create mode 100644 tests/run/i9175.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 56f9dee631d8..9dea1c486fad 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -10,6 +10,7 @@ import transform.ExplicitOuter._ import transform.ValueClasses._ import transform.TypeUtils._ import transform.ContextFunctionResults._ +import unpickleScala2.Scala2Erasure import Decorators._ import Definitions.MaxImplementedFunctionArity import scala.annotation.tailrec @@ -184,6 +185,10 @@ object TypeErasure { def valueErasure(tp: Type)(using Context): Type = erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + /** The erasure that Scala 2 would use for this type. */ + def scala2Erasure(tp: Type)(using Context): Type = + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx) + /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = valueErasure(tp) match @@ -502,8 +507,11 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst this(defn.FunctionType(paramss.head.length, isContextual = res.isImplicitMethod, isErased = res.isErasedMethod)) case tp: TypeProxy => this(tp.underlying) - case AndType(tp1, tp2) => - erasedGlb(this(tp1), this(tp2), sourceLanguage.isJava) + case tp @ AndType(tp1, tp2) => + if sourceLanguage.isScala2 then + this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp))) + else + erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava) case OrType(tp1, tp2) => TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Erasure.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Erasure.scala new file mode 100644 index 000000000000..5dde517c4864 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Erasure.scala @@ -0,0 +1,258 @@ +package dotty.tools +package dotc +package core +package unpickleScala2 + +import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Phases._ +import Decorators._ +import backend.sjs.JSDefinitions +import scala.collection.mutable.ListBuffer + +/** Erasure logic specific to Scala 2 symbols. */ +object Scala2Erasure: + /** Is this a supported Scala 2 refinement or parent of such a type? + * + * We do not allow types that look like: + * ((A with B) @foo) with C + * or: + * (A { type X <: ... })#X with C` + * + * as it would make our implementation of Scala 2 intersection erasure + * significantly more complicated. The problem is that each textual + * appearance of an intersection or refinement in a parent corresponds to a + * fresh instance of RefinedType (because Scala 2 does not hash-cons these + * types) with a fresh synthetic class symbol, thus affecting the result of + * `isNonBottomSubClass`. To complicate the matter, the Scala 2 UnCurry phase + * will also recursively dealias parent types, thus creating distinct class + * symbols even in situations where the same type alias is used to refer to a + * given refinement. Note that types like `(A with B) with C` do not run into + * these issues because they get flattened into a single RefinedType with + * three parents, cf `flattenedParents`. + * + * See sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala for examples. + * + * @throws TypeError if this type is unsupported. + */ + def checkSupported(tp: Type)(using Context): Unit = tp match + case AndType(tp1, tp2) => + checkSupported(tp1) + checkSupported(tp2) + case RefinedType(parent, _, _) => + checkSupported(parent) + case AnnotatedType(parent, _) if parent.dealias.isInstanceOf[Scala2RefinedType] => + throw new TypeError(i"Unsupported Scala 2 type: Component $parent of intersection is annotated.") + case tp @ TypeRef(prefix, _) if !tp.symbol.exists && prefix.dealias.isInstanceOf[Scala2RefinedType] => + throw new TypeError(i"Unsupported Scala 2 type: Prefix $prefix of intersection component is an intersection or refinement.") + case _ => + + /** A type that would be represented as a RefinedType in Scala 2. + * + * The `RefinedType` of Scala 2 contains both a list of parents + * and a list of refinements, intersections are represented as a RefinedType + * with no refinements. + */ + type Scala2RefinedType = RefinedType | AndType + + /** A TypeRef that is known to represent a member of a structural type. */ + type StructuralRef = TypeRef + + /** The equivalent of a Scala 2 type symbol. + * + * In some situations, nsc will create a symbol for a type where we wouldn't: + * + * - `A with B with C { ... }` is represented with a RefinedType whose + * symbol is a fresh class symbol whose parents are `A`, `B`, `C`. + * - Structural members also get their own symbols. + * + * To emulate this, we simply use the type itself as a stand-in for its symbol. + * + * See also `sameSymbol` which determines if two pseudo-symbols are really the same. + */ + type PseudoSymbol = Symbol | StructuralRef | Scala2RefinedType + + /** The pseudo symbol of `tp`, see `PseudoSymbol`. + * + * The pseudo-symbol representation of a given type is chosen such that + * `isNonBottomSubClass` behaves like it would in Scala 2, in particular + * this lets us strip all aliases. + */ + def pseudoSymbol(tp: Type)(using Context): PseudoSymbol = tp.widenDealias match + case tpw: Scala2RefinedType => + checkSupported(tpw) + tpw + case tpw: TypeRef => + val sym = tpw.symbol + if !sym.exists then + // Since we don't have symbols for structural type members we use the + // type itself and rely on `sameSymbol` to determine whether two + // such types would be represented with the same Scala 2 symbol. + tpw + else + sym + case tpw: TypeProxy => + pseudoSymbol(tpw.underlying) + case tpw: JavaArrayType => + defn.ArrayClass + case tpw: OrType => + pseudoSymbol(TypeErasure.scala2Erasure(tpw)) + case tpw: ErrorType => + defn.ObjectClass + case tpw => + throw new Error(s"Internal error: unhandled class ${tpw.getClass} for type $tpw in pseudoSymbol($tp)") + + extension (psym: PseudoSymbol)(using Context) + /** Would these two pseudo-symbols be represented with the same symbol in Scala 2? */ + def sameSymbol(other: PseudoSymbol): Boolean = + // Pattern match on (psym1, psym2) desugared by hand to avoid allocating a tuple + if psym.isInstanceOf[StructuralRef] && other.isInstanceOf[StructuralRef] then + val tp1 = psym.asInstanceOf[StructuralRef] + val tp2 = other.asInstanceOf[StructuralRef] + // Two structural members will have the same Scala 2 symbol if they + // point to the same member. We can't just call `=:=` since different + // prefixes will still have the same symbol. + (tp1.name eq tp2.name) && pseudoSymbol(tp1.prefix).sameSymbol(pseudoSymbol(tp2.prefix)) + else + // We intentionally use referential equality here even though we may end + // up comparing two equivalent intersection types, because Scala 2 will + // create fresh symbols for each appearance of an intersection type in + // source code. + psym eq other + + /** Is this a class symbol? Also returns true for refinements + * since they get a class symbol in Scala 2. + */ + def isClass: Boolean = psym match + case sym: Symbol => + sym.isClass + case _: Scala2RefinedType => + true + case _ => + false + + /** Is this a trait symbol? */ + def isTrait: Boolean = psym match + case sym: Symbol => + sym.is(Trait) + case _ => + false + + /** An emulation of `Symbol#isNonBottomSubClass` from Scala 2. + * + * The documentation of the original method is: + * + * > Is this class symbol a subclass of that symbol, + * > and is this class symbol also different from Null or Nothing? + * + * Which sounds fine, except that it is also used with non-class symbols, + * so what does it do then? Its implementation delegates to `Type#baseTypeSeq` + * whose documentation states: + * + * > The base type sequence of T is the smallest set of [...] class types Ti, so that [...] + * + * But this is also wrong: the sequence returned by `baseTypeSeq` can + * contain non-class symbols. + * + * Given that we cannot rely on the documentation and that the + * implementation is extremely complex, this reimplementation is mostly + * based on reverse-engineering rules derived from the observed behavior of + * the original method. + */ + def isNonBottomSubClass(that: PseudoSymbol): Boolean = + /** Recurse on the upper-bound of `psym`: an abstract type is a sub of a + * pseudo-symbol, if its upper-bound is a sub of that pseudo-symbol. + */ + def goUpperBound(psym: Symbol | StructuralRef): Boolean = + val info = psym match + case sym: Symbol => sym.info + case tp: StructuralRef => tp.info + info match + case info: TypeBounds => + go(pseudoSymbol(info.hi)) + case _ => + false + + def go(psym: PseudoSymbol): Boolean = + psym.sameSymbol(that) || + // As mentioned in the documentation of `Scala2RefinedType`, in Scala 2 + // these types get their own unique synthetic class symbol, therefore they + // don't have any sub-class Note that we must return false even if the lhs + // is an abstract type upper-bounded by this refinement, since each + // textual appearance of a refinement will have its own class symbol. + !that.isInstanceOf[Scala2RefinedType] && + psym.match + case sym1: Symbol => that match + case sym2: Symbol => + if sym1.isClass && sym2.isClass then + sym1.derivesFrom(sym2) + else if !sym1.isClass then + goUpperBound(sym1) + else + // sym2 is an abstract type, return false because + // `isNonBottomSubClass` in Scala 2 never considers a class C to + // be a a sub of an abstract type T, even if it was declared as + // `type T >: C`. + false + case _ => + goUpperBound(sym1) + case tp1: StructuralRef => + goUpperBound(tp1) + case tp1: RefinedType => + go(pseudoSymbol(tp1.parent)) + case AndType(tp11, tp12) => + go(pseudoSymbol(tp11)) || go(pseudoSymbol(tp12)) + end go + + go(psym) + end isNonBottomSubClass + end extension + + /** An emulation of `Erasure#intersectionDominator` from Scala 2. + * + * Accurately reproducing the behavior of this method is extremely difficult + * because it operates on the symbols of the _non-erased_ parent types, an + * implementation detail of the compiler. Furthermore, these non-class + * symbols are passed to methods such as `isNonBottomSubClass` whose behavior + * is only specified for class symbols. Therefore, the accuracy of this + * method cannot be guaranteed, the best we can do is make sure it works on + * as many test cases as possible which can be run from sbt using: + * > sbt-dotty/scripted scala2-compat/erasure + * + * The body of this method is made to look as much as the Scala 2 version as + * possible to make them easier to compare, cf: + * https://github.com/scala/scala/blob/v2.13.5/src/reflect/scala/reflect/internal/transform/Erasure.scala#L356-L389 + */ + def intersectionDominator(parents: List[Type])(using Context): Type = + val psyms = parents.map(pseudoSymbol) + if (psyms.contains(defn.ArrayClass)) { + defn.ArrayOf( + intersectionDominator(parents.collect { case defn.ArrayOf(arg) => arg })) + } else { + def isUnshadowed(psym: PseudoSymbol) = + !(psyms.exists(qsym => !psym.sameSymbol(qsym) && qsym.isNonBottomSubClass(psym))) + val cs = parents.iterator.filter { p => + val psym = pseudoSymbol(p) + psym.isClass && !psym.isTrait && isUnshadowed(psym) + } + (if (cs.hasNext) cs else parents.iterator.filter(p => isUnshadowed(pseudoSymbol(p)))).next() + } + + /** A flattened list of parents of this intersection. + * + * Mimic what Scala 2 does: intersections like `A with (B with C)` are + * flattened to three parents. + */ + def flattenedParents(tp: AndType)(using Context): List[Type] = + val parents = ListBuffer[Type]() + + def collect(parent: Type, parents: ListBuffer[Type]): Unit = parent.dealiasKeepAnnots match + case AndType(tp1, tp2) => + collect(tp1, parents) + collect(tp2, parents) + case _ => + checkSupported(parent) + parents += parent + + collect(tp, parents) + parents.toList + end flattenedParents +end Scala2Erasure diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt new file mode 100644 index 000000000000..1582924e0f41 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/build.sbt @@ -0,0 +1,15 @@ +lazy val scala2Lib = project.in(file("scala2Lib")) + .settings( + scalaVersion := "2.13.2" + ) + +lazy val dottyApp = project.in(file("dottyApp")) + .dependsOn(scala2Lib) + .settings( + scalaVersion := sys.props("plugin.scalaVersion"), + // https://github.com/sbt/sbt/issues/5369 + projectDependencies := { + projectDependencies.value.map(_.withDottyCompat(scalaVersion.value)) + } + ) + diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala b/sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala new file mode 100644 index 000000000000..341c047d29bd --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/changes/Main.scala @@ -0,0 +1,18 @@ +object Main { + def main(args: Array[String]): Unit = { + val z = new scala2Lib.Z + + def dummy[T]: T = null.asInstanceOf[T] + + // None of these method calls should typecheck, see `Scala2Erasure#supportedType` + z.b_04(dummy) + z.b_04X(dummy) + z.b_05(dummy) + z.a_48(dummy) + z.c_49(dummy) + z.a_51(dummy) + z.a_53(dummy) + z.b_56(dummy) + z.a_57(dummy) + } +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala b/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala new file mode 100644 index 000000000000..e665b1e4dfb8 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala @@ -0,0 +1,149 @@ +// Keep synchronized with scala2Lib/Api.scala +package dottyApp + +class foo extends scala.annotation.StaticAnnotation + +trait A +trait B +trait SubB extends B +trait C +trait Cov[+T] + +class D + +class VC(val self: A) extends AnyVal + +class Outer { + class E + trait F extends E +} + +// The parameter type of `a_XX` should erase to A, `b_XX` to `B`, etc. +// This is enforced by dottyApp/Main.scala +class Z { + def a_01(a: A with B): Unit = {} + def b_02X(b: B with A): Unit = {} + def a_02(a: A with B with A): Unit = {} + def a_03(a: A with (B with A)): Unit = {} + def a_04(b: A with (B with A) @foo): Unit = {} + def a_04X(b: A with (B with C) @foo): Unit = {} + def a_05(b: A with (B with A) @foo with (C with B with A) @foo): Unit = {} + + type T1 <: A with B + def a_06(a: T1): Unit = {} + + type S <: B with T1 + def b_07(a: S): Unit = {} + + type T2 <: B with A + type U <: T2 with S + def b_08(b: U): Unit = {} + + val singB: B = new B {} + def a_09(a: A with singB.type): Unit = {} + def b_10(b: singB.type with A): Unit = {} + + type V >: SubB <: B + def b_11(b: V): Unit = {} + def subb_12(b: V with SubB): Unit = {} + + def d_13(d: D with A): Unit = {} + def d_14(d: A with D): Unit = {} + + val singD: D = new D {} + def d_13x(d: singD.type with A): Unit = {} + def d_14x(d: A with singD.type): Unit = {} + + type DEq = D + def d_15(d: A with DEq): Unit = {} + def d_16(d: A with (DEq @foo)): Unit = {} + def d_17(d: DEq with A): Unit = {} + def d_18(d: (DEq @foo) with A): Unit = {} + + val singDEq: DEq @foo = new D {} + def d_15b(d: A with singDEq.type): Unit = {} + def d_16b(d: A with (singDEq.type @foo)): Unit = {} + + type DSub <: D + def d_19(a: A with DSub): Unit = {} + def d_19x(d: DSub with A): Unit = {} + def d_20(z: DSub with Z): Unit = {} + + type W1 <: A with Cov[Any] + type X1 <: Cov[Int] with W1 + def cov_21(a: X1): Unit = {} + + type W2 <: A with Cov[Any] + type X2 <: Cov[Int] with W2 + def cov_22(a: X2): Unit = {} + + def z_23(z: A with this.type): Unit = {} + def z_24(z: this.type with A): Unit = {} + + def a_25(b: A with (B { type T })): Unit = {} + def a_26(a: (A { type T }) with ((B with A) { type T })): Unit = {} + + def b_27(a: VC with B): Unit = {} + def b_28(a: B with VC): Unit = {} + + val o1: Outer = new Outer + val o2: Outer = new Outer + def f_29(f: o1.E with o1.F): Unit = {} + def f_30(f: o1.F with o1.E): Unit = {} + def f_31(f: o1.E with o2.F): Unit = {} + def f_32(f: o2.F with o1.E): Unit = {} + def f_33(f: Outer#E with Outer#F): Unit = {} + def f_34(f: Outer#F with Outer#E): Unit = {} + + val structural1: { type DSub <: D } = new { type DSub <: D } + def d_35(a: A with structural1.DSub): Unit = {} + def d_36(a: structural1.DSub with A): Unit = {} + def z_37(z: Z with structural1.DSub): Unit = {} + def d_38(z: structural1.DSub with Z): Unit = {} + + val structural2: { type SubCB <: C with B } = new { type SubCB <: C with B } + def c_39(c: structural2.SubCB with B): Unit = {} + def b_40(c: B with structural2.SubCB): Unit = {} + + val structural3a: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB } + val structural3b: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB } + def b_41(c: structural3a.SubB with structural3a.SubCB): Unit = {} + def c_42(c: structural3a.SubCB with structural3a.SubB): Unit = {} + def b_43(b: structural3a.SubB with structural3b.SubCB): Unit = {} + def c_44(c: structural3b.SubCB with structural3a.SubB): Unit = {} + + type SubStructural <: C with structural3a.SubB + def b_45(x: structural3a.SubB with SubStructural): Unit = {} + def b_46(x: structural3b.SubB with SubStructural): Unit = {} + + type Rec1 <: A with B + type Rec2 <: C with Rec1 + def a_47(a: A with B with Rec2): Unit = {} + def a_48(a: (A with B) @foo with Rec2): Unit = {} + + type F1 = A with B + type F2 = A with B + type Rec3 <: F1 + type Rec4 <: C with Rec3 + def a_49(a: F1 @foo with Rec4): Unit = {} + def a_50(a: F1 with Rec4): Unit = {} + def a_51(a: F2 @foo with Rec4): Unit = {} + def a_52(a: F2 with Rec4): Unit = {} + + type AA = A + type F3 = AA with B + type Rec5 <: F3 + type Rec6 <: C with Rec5 + def a_53(a: F3 @foo with Rec6): Unit = {} + def a_54(a: F3 with Rec6): Unit = {} + + val structural4a: { type M[X] <: A } = new { type M[X] <: A } + val structural4b: { type N <: B with structural4a.M[Int] } = new { type N <: B with structural4a.M[Int] } + def a_55(x: structural4a.M[Any] with structural4b.N): Unit = {} + + type Bla = A { type M[X] <: A } + def a_56(x: Bla#M[Any] with ({ type N <: B with Bla#M[Int] })#N): Unit = {} + type AEq = A + type Bla2 = AEq { type M[X] <: A } + def a_57(x: Bla2#M[Any] with ({ type N <: B with Bla2#M[Int] })#N): Unit = {} +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala b/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala new file mode 100644 index 000000000000..6f2b2507a4b0 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala @@ -0,0 +1,82 @@ +object Main { + def main(args: Array[String]): Unit = { + val z = new scala2Lib.Z + + def dummy[T]: T = null.asInstanceOf[T] + + // Commented out lines intentionally do not typecheck and are + // tested in changes/Main.scala + z.a_01(dummy) + z.a_02(dummy) + z.b_02X(dummy) + z.a_03(dummy) + // z.b_04(dummy) + // z.b_04X(dummy) + // z.b_05(dummy) + z.a_06(dummy) + z.a_07(dummy) + z.b_08(dummy) + z.a_09(dummy) + z.b_10(dummy) + z.b_11(dummy) + z.b_12(dummy) + z.d_13(dummy) + z.d_14(dummy) + z.d_15(dummy) + z.d_15b(dummy) + z.d_16(dummy) + z.d_16b(dummy) + z.d_17(dummy) + z.d_18(dummy) + z.a_19(dummy) + z.d_19x(dummy) + z.z_20(dummy) + z.a_21(dummy) + z.a_22(dummy) + z.z_23(dummy) + z.z_24(dummy) + z.b_25(dummy) + z.a_26(dummy) + z.a_27(dummy) + z.a_28(dummy) + z.f_29(dummy) + z.f_30(dummy) + z.f_31(dummy) + z.f_32(dummy) + z.f_33(dummy) + z.f_34(dummy) + z.a_35(dummy) + z.d_36(dummy) + z.z_37(dummy) + z.z_38(dummy) + z.c_39(dummy) + z.c_40(dummy) + z.c_41(dummy) + z.c_42(dummy) + z.b_43(dummy) + z.c_44(dummy) + z.c_45(dummy) + z.b_46(dummy) + z.c_47(dummy) + // z.a_48(dummy) + // z.c_49(dummy) + z.c_50(dummy) + // z.a_51(dummy) + z.c_52(dummy) + // z.a_53(dummy) + z.c_54(dummy) + z.b_55(dummy) + // z.b_56(dummy) + // z.a_57(dummy) + + val methods = classOf[scala2Lib.Z].getDeclaredMethods.toList ++ classOf[dottyApp.Z].getDeclaredMethods.toList + methods.foreach { m => + m.getName match { + case s"${prefix}_${suffix}" => + val paramClass = m.getParameterTypes()(0).getSimpleName + assert(prefix == paramClass.toLowerCase, s"Method `$m` erased to `$paramClass` which does not match its prefix `$prefix`") + case _ => + } + } + } +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/project/plugins.sbt b/sbt-dotty/sbt-test/scala2-compat/erasure/project/plugins.sbt new file mode 100644 index 000000000000..c17caab2d98c --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version")) diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala b/sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala new file mode 100644 index 000000000000..7ea084a99b4e --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala @@ -0,0 +1,149 @@ +// Keep synchronized with dottyApp/Api.scala +package scala2Lib + +class foo extends scala.annotation.StaticAnnotation + +trait A +trait B +trait SubB extends B +trait C +trait Cov[+T] + +class D + +class VC(val self: A) extends AnyVal + +class Outer { + class E + trait F extends E +} + +// The parameter type of `a_XX` should erase to A, `b_XX` to `B`, etc. +// This is enforced by dottyApp/Main.scala +class Z { + def a_01(a: A with B): Unit = {} + def b_02X(b: B with A): Unit = {} + def a_02(a: A with B with A): Unit = {} + def a_03(a: A with (B with A)): Unit = {} + def b_04(b: A with (B with A) @foo): Unit = {} + def b_04X(b: A with (B with C) @foo): Unit = {} + def b_05(b: A with (B with A) @foo with (C with B with A) @foo): Unit = {} + + type T1 <: A with B + def a_06(a: T1): Unit = {} + + type S <: B with T1 + def a_07(a: S): Unit = {} + + type T2 <: B with A + type U <: T2 with S + def b_08(b: U): Unit = {} + + val singB: B = new B {} + def a_09(a: A with singB.type): Unit = {} + def b_10(b: singB.type with A): Unit = {} + + type V >: SubB <: B + def b_11(b: V): Unit = {} + def b_12(b: V with SubB): Unit = {} + + def d_13(d: D with A): Unit = {} + def d_14(d: A with D): Unit = {} + + val singD: D = new D {} + def d_13x(d: singD.type with A): Unit = {} + def d_14x(d: A with singD.type): Unit = {} + + type DEq = D + def d_15(d: A with DEq): Unit = {} + def d_16(d: A with (DEq @foo)): Unit = {} + def d_17(d: DEq with A): Unit = {} + def d_18(d: (DEq @foo) with A): Unit = {} + + val singDEq: DEq @foo = new D {} + def d_15b(d: A with singDEq.type): Unit = {} + def d_16b(d: A with (singDEq.type @foo)): Unit = {} + + type DSub <: D + def a_19(a: A with DSub): Unit = {} + def d_19x(d: DSub with A): Unit = {} + def z_20(z: DSub with Z): Unit = {} + + type W1 <: A with Cov[Any] + type X1 <: Cov[Int] with W1 + def a_21(a: X1): Unit = {} + + type W2 <: A with Cov[Any] + type X2 <: Cov[Int] with W2 + def a_22(a: X2): Unit = {} + + def z_23(z: A with this.type): Unit = {} + def z_24(z: this.type with A): Unit = {} + + def b_25(b: A with (B { type T })): Unit = {} + def a_26(a: (A { type T }) with ((B with A) { type T })): Unit = {} + + def a_27(a: VC with B): Unit = {} + def a_28(a: B with VC): Unit = {} + + val o1: Outer = new Outer + val o2: Outer = new Outer + def f_29(f: o1.E with o1.F): Unit = {} + def f_30(f: o1.F with o1.E): Unit = {} + def f_31(f: o1.E with o2.F): Unit = {} + def f_32(f: o2.F with o1.E): Unit = {} + def f_33(f: Outer#E with Outer#F): Unit = {} + def f_34(f: Outer#F with Outer#E): Unit = {} + + val structural1: { type DSub <: D } = new { type DSub <: D } + def a_35(a: A with structural1.DSub): Unit = {} + def d_36(a: structural1.DSub with A): Unit = {} + def z_37(z: Z with structural1.DSub): Unit = {} + def z_38(z: structural1.DSub with Z): Unit = {} + + val structural2: { type SubCB <: C with B } = new { type SubCB <: C with B } + def c_39(c: structural2.SubCB with B): Unit = {} + def c_40(c: B with structural2.SubCB): Unit = {} + + val structural3a: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB } + val structural3b: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB } + def c_41(c: structural3a.SubB with structural3a.SubCB): Unit = {} + def c_42(c: structural3a.SubCB with structural3a.SubB): Unit = {} + def b_43(b: structural3a.SubB with structural3b.SubCB): Unit = {} + def c_44(c: structural3b.SubCB with structural3a.SubB): Unit = {} + + type SubStructural <: C with structural3a.SubB + def c_45(x: structural3a.SubB with SubStructural): Unit = {} + def b_46(x: structural3b.SubB with SubStructural): Unit = {} + + type Rec1 <: A with B + type Rec2 <: C with Rec1 + def c_47(a: A with B with Rec2): Unit = {} + def a_48(a: (A with B) @foo with Rec2): Unit = {} + + type F1 = A with B + type F2 = A with B + type Rec3 <: F1 + type Rec4 <: C with Rec3 + def c_49(a: F1 @foo with Rec4): Unit = {} + def c_50(a: F1 with Rec4): Unit = {} + def a_51(a: F2 @foo with Rec4): Unit = {} + def c_52(a: F2 with Rec4): Unit = {} + + type AA = A + type F3 = AA with B + type Rec5 <: F3 + type Rec6 <: C with Rec5 + def a_53(a: F3 @foo with Rec6): Unit = {} + def c_54(a: F3 with Rec6): Unit = {} + + val structural4a: { type M[X] <: A } = new { type M[X] <: A } + val structural4b: { type N <: B with structural4a.M[Int] } = new { type N <: B with structural4a.M[Int] } + def b_55(x: structural4a.M[Any] with structural4b.N): Unit = {} + + type Bla = A { type M[X] <: A } + def b_56(x: Bla#M[Any] with ({ type N <: B with Bla#M[Int] })#N): Unit = {} + type AEq = A + type Bla2 = AEq { type M[X] <: A } + def a_57(x: Bla2#M[Any] with ({ type N <: B with Bla2#M[Int] })#N): Unit = {} +} diff --git a/sbt-dotty/sbt-test/scala2-compat/erasure/test b/sbt-dotty/sbt-test/scala2-compat/erasure/test new file mode 100644 index 000000000000..3ef735f7d6a1 --- /dev/null +++ b/sbt-dotty/sbt-test/scala2-compat/erasure/test @@ -0,0 +1,3 @@ +> dottyApp/run +$ copy-file changes/Main.scala dottyApp/Main.scala +-> dottyApp/compile diff --git a/tests/run/array-erasure.scala b/tests/run/array-erasure.scala new file mode 100644 index 000000000000..ba5f9e12c39c --- /dev/null +++ b/tests/run/array-erasure.scala @@ -0,0 +1,45 @@ +object Test { + def arr0[T](x: Array[T]) = { + assert(x(0) == 0) + x.sameElements(x) + x match { + case x: Array[Int] => + x(0) = 1 + x.sameElements(x) + } + assert(x(0) == 1) + } + + def arr1(x: Array[Int]) = { + assert(x(0) == 1) + x.sameElements(x) + x match { + case x: Array[_] => + x(0) = 2 + x.sameElements(x) + } + assert(x(0) == 2) + } + + def arr2[T](x: T) = { + x match { + case x: Array[_] => + assert(x(0) == 2) + x.sameElements(x) + } + x match { + case x: Array[Int] => + assert(x(0) == 2) + x(0) = 3 + x.sameElements(x) + } + } + + def main(args: Array[String]): Unit = { + val x: Array[Int] = Array(0) + + arr0(x) + arr1(x) + arr2(x) + } +} diff --git a/tests/run/i9175.scala b/tests/run/i9175.scala new file mode 100644 index 000000000000..d099de72a96d --- /dev/null +++ b/tests/run/i9175.scala @@ -0,0 +1,10 @@ +import scala.collection.immutable.SortedMap + +object Test { + + def main(args: Array[String]): Unit = { + val sortedMap = SortedMap("a" -> 1, "b" -> 2, "c" -> 3) + val empty = sortedMap.empty + println(empty) + } +} From 943636ff3be1ba52daf6fb409ddf7101aae8272b Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Mon, 8 Mar 2021 13:40:04 +0100 Subject: [PATCH 5/5] Properly handle value classes appearing in Java code Previously their signature was incorrectly computed (the value class was erased to its underlying type) which lead to valid Java overloads being seen as double definitions. --- compiler/src/dotty/tools/dotc/core/TypeErasure.scala | 4 ++-- tests/run/java-vc-overload/A_2.java | 4 ++++ tests/run/java-vc-overload/Test_3.scala | 7 +++++++ tests/run/java-vc-overload/VC_1.scala | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 tests/run/java-vc-overload/A_2.java create mode 100644 tests/run/java-vc-overload/Test_3.scala create mode 100644 tests/run/java-vc-overload/VC_1.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9dea1c486fad..f0af9e7ffc28 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -197,7 +197,7 @@ object TypeErasure { def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = { val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava) - val erase = erasureFn(sourceLanguage, semiEraseVCs = true, isConstructor = false, wildcardOK = true) + val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, wildcardOK = true) erase.sigName(normTp)(using preErasureCtx) } @@ -667,7 +667,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst if (!info.exists) assert(false, i"undefined: $tp with symbol $sym") return sigName(info) } - if (isDerivedValueClass(sym)) { + if (semiEraseVCs && isDerivedValueClass(sym)) { val erasedVCRef = eraseDerivedValueClass(tp) if (erasedVCRef.exists) return sigName(erasedVCRef) } diff --git a/tests/run/java-vc-overload/A_2.java b/tests/run/java-vc-overload/A_2.java new file mode 100644 index 000000000000..caa333f45301 --- /dev/null +++ b/tests/run/java-vc-overload/A_2.java @@ -0,0 +1,4 @@ +public class A_2 { + public void foo(VC vc) {} + public void foo(String string) {} +} diff --git a/tests/run/java-vc-overload/Test_3.scala b/tests/run/java-vc-overload/Test_3.scala new file mode 100644 index 000000000000..34669f91f7f8 --- /dev/null +++ b/tests/run/java-vc-overload/Test_3.scala @@ -0,0 +1,7 @@ +object Test { + def main(args: Array[String]): Unit = { + val a = new A_2 + a.foo("") + a.foo(new VC("")) + } +} diff --git a/tests/run/java-vc-overload/VC_1.scala b/tests/run/java-vc-overload/VC_1.scala new file mode 100644 index 000000000000..e854861d77dc --- /dev/null +++ b/tests/run/java-vc-overload/VC_1.scala @@ -0,0 +1 @@ +class VC(val value: String) extends AnyVal