Skip to content

Correctly erase Scala 2 intersection types #11603

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 5 commits into from
Mar 9, 2021
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
43 changes: 22 additions & 21 deletions compiler/src/dotty/tools/dotc/core/Denotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,9 @@ object Denotations {
*/
def prefix: Type = NoPrefix

/** Either the Scala or Java signature of the info, depending on where the
* symbol is defined.
/** 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
Expand All @@ -595,17 +596,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 || !this.isInstanceOf[SymDenotation] 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")
Expand Down Expand Up @@ -1013,36 +1014,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 =>
Expand Down
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/core/Signature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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] {
Expand Down
127 changes: 85 additions & 42 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,45 @@ import transform.ExplicitOuter._
import transform.ValueClasses._
import transform.TypeUtils._
import transform.ContextFunctionResults._
import unpickleScala2.Scala2Erasure
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
Expand Down Expand Up @@ -107,28 +142,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) =
Expand All @@ -139,25 +175,29 @@ 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]]).
*
* @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)

/** 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
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 = !sourceLanguage.isJava, isConstructor = false, wildcardOK = true)
erase.sigName(normTp)(using preErasureCtx)
}

Expand All @@ -181,15 +221,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(
Expand Down Expand Up @@ -391,18 +429,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:
*
Expand Down Expand Up @@ -450,7 +490,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 =>
Expand All @@ -467,13 +507,16 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
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), 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 =>
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 {
Expand Down Expand Up @@ -516,8 +559,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 = {
Expand All @@ -544,7 +587,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
Expand Down Expand Up @@ -596,7 +639,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
Expand Down Expand Up @@ -624,7 +667,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
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)
}
Expand Down
Loading