Skip to content

Commit 2c901c8

Browse files
committed
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. Having to juggle three kinds of erasure modes isn't great, we really want to distinguish between Scala 2 and 3 erasure (cf the follow-up commits in this PR), but it's worth asking whether we couldn't make Scala 3 erasure a superset of Java erasure which would allow us to go back to only two modes. This is something I'm investigating for a follow-up PR.
1 parent 65a8870 commit 2c901c8

File tree

5 files changed

+124
-79
lines changed

5 files changed

+124
-79
lines changed

compiler/src/dotty/tools/dotc/core/Denotations.scala

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -582,8 +582,7 @@ object Denotations {
582582
*/
583583
def prefix: Type = NoPrefix
584584

585-
/** Either the Scala or Java signature of the info, depending on where the
586-
* symbol is defined.
585+
/** The symbol-specific signature of the info.
587586
*
588587
* Invariants:
589588
* - Before erasure, the signature of a denotation is always equal to the
@@ -595,17 +594,17 @@ object Denotations {
595594
* SingleDenotations will have distinct signatures (cf #9050).
596595
*/
597596
final def signature(using Context): Signature =
598-
signature(isJava = !isType && symbol.is(JavaDefined))
597+
signature(sourceLanguage = if isType then SourceLanguage.Scala3 else SourceLanguage(symbol))
599598

600-
/** Overload of `signature` which lets the caller pick between the Java and
601-
* Scala signature of the info. Useful to match denotations defined in
599+
/** Overload of `signature` which lets the caller pick the language used
600+
* to compute the signature of the info. Useful to match denotations defined in
602601
* different classes (see `matchesLoosely`).
603602
*/
604-
def signature(isJava: Boolean)(using Context): Signature =
603+
def signature(sourceLanguage: SourceLanguage)(using Context): Signature =
605604
if (isType) Signature.NotAMethod // don't force info if this is a type denotation
606605
else info match {
607606
case info: MethodOrPoly =>
608-
try info.signature(isJava)
607+
try info.signature(sourceLanguage)
609608
catch { // !!! DEBUG
610609
case scala.util.control.NonFatal(ex) =>
611610
report.echo(s"cannot take signature of $info")
@@ -1013,25 +1012,27 @@ object Denotations {
10131012

10141013
/** `matches` without a target name check.
10151014
*
1016-
* We consider a Scala method and a Java method to match if they have
1017-
* matching Scala signatures. This allows us to override some Java
1018-
* definitions even if they have a different erasure (see i8615b,
1019-
* i9109b), Erasure takes care of adding any necessary bridge to make
1020-
* this work at runtime.
1015+
* For definitions coming from different languages, we pick a common
1016+
* language to compute their signatures. This allows us for example to
1017+
* override some Java definitions from Scala even if they have a different
1018+
* erasure (see i8615b, i9109b), Erasure takes care of adding any necessary
1019+
* bridge to make this work at runtime.
10211020
*/
10221021
def matchesLoosely(other: SingleDenotation)(using Context): Boolean =
10231022
if isType then true
10241023
else
1025-
val isJava = symbol.is(JavaDefined)
1026-
val otherIsJava = other.symbol.is(JavaDefined)
1027-
val useJavaSig = isJava && otherIsJava
1028-
val sig = signature(isJava = useJavaSig)
1029-
val otherSig = other.signature(isJava = useJavaSig)
1024+
val thisLanguage = SourceLanguage(symbol)
1025+
val otherLanguage = SourceLanguage(other.symbol)
1026+
val commonLanguage = SourceLanguage.commonLanguage(thisLanguage, otherLanguage)
1027+
val sig = signature(commonLanguage)
1028+
val otherSig = other.signature(commonLanguage)
10301029
sig.matchDegree(otherSig) match
10311030
case FullMatch =>
10321031
true
10331032
case MethodNotAMethodMatch =>
10341033
!ctx.erasedTypes && {
1034+
val isJava = thisLanguage.isJava
1035+
val otherIsJava = otherLanguage.isJava
10351036
// A Scala zero-parameter method and a Scala non-method always match.
10361037
if !isJava && !otherIsJava then
10371038
true

compiler/src/dotty/tools/dotc/core/Signature.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ case class Signature(paramsSig: List[ParamSig], resSig: TypeName) {
109109
*
110110
* Like Signature#apply, the result is only cacheable if `isUnderDefined == false`.
111111
*/
112-
def prependTermParams(params: List[Type], isJava: Boolean)(using Context): Signature =
113-
Signature(params.map(p => sigName(p, isJava)) ::: paramsSig, resSig)
112+
def prependTermParams(params: List[Type], sourceLanguage: SourceLanguage)(using Context): Signature =
113+
Signature(params.map(p => sigName(p, sourceLanguage)) ::: paramsSig, resSig) // XX
114114

115115
/** Construct a signature by prepending the length of a type parameter section
116116
* to the parameter part of this signature.
@@ -164,9 +164,9 @@ object Signature {
164164
* otherwise the signature will change once the contained type variables have
165165
* been instantiated.
166166
*/
167-
def apply(resultType: Type, isJava: Boolean)(using Context): Signature = {
167+
def apply(resultType: Type, sourceLanguage: SourceLanguage)(using Context): Signature = {
168168
assert(!resultType.isInstanceOf[ExprType])
169-
apply(Nil, sigName(resultType, isJava))
169+
apply(Nil, sigName(resultType, sourceLanguage))
170170
}
171171

172172
val lexicographicOrdering: Ordering[Signature] = new Ordering[Signature] {

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,40 @@ import Decorators._
1414
import Definitions.MaxImplementedFunctionArity
1515
import scala.annotation.tailrec
1616

17+
/** The language in which the definition being erased was written. */
18+
enum SourceLanguage:
19+
case Java, Scala2, Scala3
20+
def isJava: Boolean = this eq Java
21+
def isScala2: Boolean = this eq Scala2
22+
def isScala3: Boolean = this eq Scala3
23+
object SourceLanguage:
24+
/** The language in which `sym` was defined. */
25+
def apply(sym: Symbol)(using Context): SourceLanguage =
26+
if sym.is(JavaDefined) then
27+
SourceLanguage.Java
28+
// Scala 2 methods don't have Inline set, except for the ones injected with `patchStdlibClass`
29+
// which are really Scala 3 methods.
30+
else if sym.isClass && sym.is(Scala2x) || (sym.maybeOwner.is(Scala2x) && !sym.is(Inline)) then
31+
SourceLanguage.Scala2
32+
else
33+
SourceLanguage.Scala3
34+
35+
/** Number of bits needed to represent this enum. */
36+
def bits: Int =
37+
val len = values.length
38+
val log2 = 31 - Integer.numberOfLeadingZeros(len)
39+
if len == 1 << log2 then
40+
log2
41+
else
42+
log2 + 1
43+
44+
/** A common language to use when matching definitions written in different
45+
* languages.
46+
*/
47+
def commonLanguage(x: SourceLanguage, y: SourceLanguage): SourceLanguage =
48+
if x.ordinal > y.ordinal then x else y
49+
end SourceLanguage
50+
1751
/** Erased types are:
1852
*
1953
* ErasedValueType
@@ -107,28 +141,30 @@ object TypeErasure {
107141
}
108142
}
109143

110-
private def erasureIdx(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) =
111-
(if (isJava) 1 else 0) +
112-
(if (semiEraseVCs) 2 else 0) +
113-
(if (isConstructor) 4 else 0) +
114-
(if (wildcardOK) 8 else 0)
144+
private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) =
145+
extension (b: Boolean) def toInt = if b then 1 else 0
146+
val sourceBits = SourceLanguage.bits
147+
sourceLanguage.ordinal
148+
+ (semiEraseVCs.toInt << sourceBits)
149+
+ (isConstructor.toInt << (sourceBits + 1))
150+
+ (wildcardOK.toInt << (sourceBits + 2))
115151

116-
private val erasures = new Array[TypeErasure](16)
152+
private val erasures = new Array[TypeErasure](1 << (SourceLanguage.bits + 3))
117153

118-
for {
119-
isJava <- List(false, true)
154+
for
155+
sourceLanguage <- SourceLanguage.values
120156
semiEraseVCs <- List(false, true)
121157
isConstructor <- List(false, true)
122158
wildcardOK <- List(false, true)
123-
}
124-
erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK)) =
125-
new TypeErasure(isJava, semiEraseVCs, isConstructor, wildcardOK)
159+
do
160+
erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)) =
161+
new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)
126162

127163
/** Produces an erasure function. See the documentation of the class [[TypeErasure]]
128164
* for a description of each parameter.
129165
*/
130-
private def erasureFn(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure =
131-
erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK))
166+
private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure =
167+
erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK))
132168

133169
/** The current context with a phase no later than erasure */
134170
def preErasureCtx(using Context) =
@@ -139,25 +175,25 @@ object TypeErasure {
139175
* @param tp The type to erase.
140176
*/
141177
def erasure(tp: Type)(using Context): Type =
142-
erasureFn(isJava = false, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx)
178+
erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx)
143179

144180
/** The value class erasure of a Scala type, where value classes are semi-erased to
145181
* ErasedValueType (they will be fully erased in [[ElimErasedValueType]]).
146182
*
147183
* @param tp The type to erase.
148184
*/
149185
def valueErasure(tp: Type)(using Context): Type =
150-
erasureFn(isJava = false, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx)
186+
erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, wildcardOK = false)(tp)(using preErasureCtx)
151187

152188
/** Like value class erasure, but value classes erase to their underlying type erasure */
153189
def fullErasure(tp: Type)(using Context): Type =
154190
valueErasure(tp) match
155191
case ErasedValueType(_, underlying) => erasure(underlying)
156192
case etp => etp
157193

158-
def sigName(tp: Type, isJava: Boolean)(using Context): TypeName = {
159-
val normTp = tp.translateFromRepeated(toArray = isJava)
160-
val erase = erasureFn(isJava, semiEraseVCs = true, isConstructor = false, wildcardOK = true)
194+
def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = {
195+
val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava)
196+
val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor = false, wildcardOK = true)
161197
erase.sigName(normTp)(using preErasureCtx)
162198
}
163199

@@ -181,15 +217,13 @@ object TypeErasure {
181217
* - For $asInstanceOf : [T]T
182218
* - For $isInstanceOf : [T]Boolean
183219
* - For all abstract types : = ?
184-
* - For Java-defined symbols: : the erasure of their type with isJava = true,
185-
* semiEraseVCs = false. Semi-erasure never happens in Java.
186-
* - For all other symbols : the semi-erasure of their types, with
187-
* isJava, isConstructor set according to symbol.
220+
*
221+
* `sourceLanguage`, `isConstructor` and `semiEraseVCs` are set based on the symbol.
188222
*/
189223
def transformInfo(sym: Symbol, tp: Type)(using Context): Type = {
190-
val isJava = sym is JavaDefined
191-
val semiEraseVCs = !isJava
192-
val erase = erasureFn(isJava, semiEraseVCs, sym.isConstructor, wildcardOK = false)
224+
val sourceLanguage = SourceLanguage(sym)
225+
val semiEraseVCs = !sourceLanguage.isJava // Java sees our value classes as regular classes.
226+
val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, wildcardOK = false)
193227

194228
def eraseParamBounds(tp: PolyType): Type =
195229
tp.derivedLambdaType(
@@ -391,18 +425,20 @@ object TypeErasure {
391425
case _ => false
392426
}
393427
}
428+
394429
import TypeErasure._
395430

396431
/**
397-
* @param isJava Arguments should be treated the way Java does it
398-
* @param semiEraseVCs If true, value classes are semi-erased to ErasedValueType
399-
* (they will be fully erased in [[ElimErasedValueType]]).
400-
* If false, they are erased like normal classes.
401-
* @param isConstructor Argument forms part of the type of a constructor
402-
* @param wildcardOK Wildcards are acceptable (true when using the erasure
403-
* for computing a signature name).
432+
* @param sourceLanguage Adapt our erasure rules to mimic what the given language
433+
* would do.
434+
* @param semiEraseVCs If true, value classes are semi-erased to ErasedValueType
435+
* (they will be fully erased in [[ElimErasedValueType]]).
436+
* If false, they are erased like normal classes.
437+
* @param isConstructor Argument forms part of the type of a constructor
438+
* @param wildcardOK Wildcards are acceptable (true when using the erasure
439+
* for computing a signature name).
404440
*/
405-
class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) {
441+
class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) {
406442

407443
/** The erasure |T| of a type T. This is:
408444
*
@@ -450,7 +486,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
450486
val tycon = tp.tycon
451487
if (tycon.isRef(defn.ArrayClass)) eraseArray(tp)
452488
else if (tycon.isRef(defn.PairClass)) erasePair(tp)
453-
else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = isJava))
489+
else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava))
454490
else if (semiEraseVCs && isDerivedValueClass(tycon.classSymbol)) eraseDerivedValueClass(tp)
455491
else apply(tp.translucentSuperType)
456492
case _: TermRef | _: ThisType =>
@@ -468,12 +504,12 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
468504
case tp: TypeProxy =>
469505
this(tp.underlying)
470506
case AndType(tp1, tp2) =>
471-
erasedGlb(this(tp1), this(tp2), isJava)
507+
erasedGlb(this(tp1), this(tp2), sourceLanguage.isJava)
472508
case OrType(tp1, tp2) =>
473509
TypeComparer.orType(this(tp1), this(tp2), isErased = true)
474510
case tp: MethodType =>
475511
def paramErasure(tpToErase: Type) =
476-
erasureFn(isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
512+
erasureFn(sourceLanguage, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
477513
val (names, formals0) = if (tp.isErasedMethod) (Nil, Nil) else (tp.paramNames, tp.paramInfos)
478514
val formals = formals0.mapConserve(paramErasure)
479515
eraseResult(tp.resultType) match {
@@ -516,8 +552,8 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
516552
private def eraseArray(tp: Type)(using Context) = {
517553
val defn.ArrayOf(elemtp) = tp
518554
if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType)
519-
else if (isUnboundedGeneric(elemtp) && !isJava) defn.ObjectType
520-
else JavaArrayType(erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp))
555+
else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType
556+
else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK)(elemtp))
521557
}
522558

523559
private def erasePair(tp: Type)(using Context): Type = {
@@ -544,7 +580,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
544580
// See doc comment for ElimByName for speculation how we could improve this.
545581
else
546582
MethodType(Nil, Nil,
547-
eraseResult(sym.info.finalResultType.translateFromRepeated(toArray = isJava)))
583+
eraseResult(sym.info.finalResultType.translateFromRepeated(toArray = sourceLanguage.isJava)))
548584
case tp1: PolyType =>
549585
eraseResult(tp1.resultType) match
550586
case rt: MethodType => rt
@@ -596,7 +632,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
596632
// correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a
597633
// constructor method should not be semi-erased.
598634
if semiEraseVCs && isConstructor && !tp.isInstanceOf[MethodOrPoly] then
599-
erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp)
635+
erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, wildcardOK).eraseResult(tp)
600636
else tp match
601637
case tp: TypeRef =>
602638
val sym = tp.symbol

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3345,6 +3345,8 @@ object Types {
33453345
private var mySignatureRunId: Int = NoRunId
33463346
private var myJavaSignature: Signature = _
33473347
private var myJavaSignatureRunId: Int = NoRunId
3348+
private var myScala2Signature: Signature = _
3349+
private var myScala2SignatureRunId: Int = NoRunId
33483350

33493351
/** If `isJava` is false, the Scala signature of this method. Otherwise, its Java signature.
33503352
*
@@ -3360,39 +3362,45 @@ object Types {
33603362
*
33613363
* @see SingleDenotation#signature
33623364
*/
3363-
def signature(isJava: Boolean)(using Context): Signature =
3364-
def computeSignature(isJava: Boolean)(using Context): Signature =
3365+
def signature(sourceLanguage: SourceLanguage)(using Context): Signature =
3366+
def computeSignature(using Context): Signature =
33653367
val resultSignature = resultType match
3366-
case tp: MethodOrPoly => tp.signature(isJava)
3368+
case tp: MethodOrPoly => tp.signature(sourceLanguage)
33673369
case tp: ExprType => tp.signature
33683370
case tp =>
33693371
if tp.isRef(defn.UnitClass) then Signature(Nil, defn.UnitClass.fullName.asTypeName)
3370-
else Signature(tp, isJava)
3372+
else Signature(tp, sourceLanguage)
33713373
this match
33723374
case tp: MethodType =>
33733375
val params = if (isErasedMethod) Nil else tp.paramInfos
3374-
resultSignature.prependTermParams(params, isJava)
3376+
resultSignature.prependTermParams(params, sourceLanguage)
33753377
case tp: PolyType =>
33763378
resultSignature.prependTypeParams(tp.paramNames.length)
33773379

3378-
if isJava then
3379-
if ctx.runId != myJavaSignatureRunId then
3380-
myJavaSignature = computeSignature(isJava)
3381-
if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId
3382-
myJavaSignature
3383-
else
3384-
if ctx.runId != mySignatureRunId then
3385-
mySignature = computeSignature(isJava)
3386-
if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId
3387-
mySignature
3380+
sourceLanguage match
3381+
case SourceLanguage.Java =>
3382+
if ctx.runId != myJavaSignatureRunId then
3383+
myJavaSignature = computeSignature
3384+
if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId
3385+
myJavaSignature
3386+
case SourceLanguage.Scala2 =>
3387+
if ctx.runId != myScala2SignatureRunId then
3388+
myScala2Signature = computeSignature
3389+
if !myScala2Signature.isUnderDefined then myScala2SignatureRunId = ctx.runId
3390+
myScala2Signature
3391+
case SourceLanguage.Scala3 =>
3392+
if ctx.runId != mySignatureRunId then
3393+
mySignature = computeSignature
3394+
if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId
3395+
mySignature
33883396
end signature
33893397

33903398
/** The Scala signature of this method. Note that two distinct Java method
33913399
* overloads may have the same Scala signature, the other overload of
33923400
* `signature` can be used to avoid ambiguity if necessary.
33933401
*/
33943402
final override def signature(using Context): Signature =
3395-
signature(isJava = false)
3403+
signature(sourceLanguage = SourceLanguage.Scala3)
33963404

33973405
final override def hashCode: Int = System.identityHashCode(this)
33983406

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,12 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
385385

386386
private def hasWriteReplace(clazz: ClassSymbol)(using Context): Boolean =
387387
clazz.membersNamed(nme.writeReplace)
388-
.filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, isJava = false))
388+
.filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, sourceLanguage = SourceLanguage.Scala3))
389389
.exists
390390

391391
private def hasReadResolve(clazz: ClassSymbol)(using Context): Boolean =
392392
clazz.membersNamed(nme.readResolve)
393-
.filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, isJava = false))
393+
.filterWithPredicate(s => s.signature == Signature(defn.AnyRefType, sourceLanguage = SourceLanguage.Scala3))
394394
.exists
395395

396396
private def writeReplaceDef(clazz: ClassSymbol)(using Context): TermSymbol =

0 commit comments

Comments
 (0)