Skip to content

Commit f2e0a4c

Browse files
authored
Merge pull request #11808 from dotty-staging/new-intersection-erasure
Associative, commutative, Java-friendly intersection erasure
2 parents 45b35b3 + 9af4bad commit f2e0a4c

File tree

15 files changed

+271
-92
lines changed

15 files changed

+271
-92
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2128,7 +2128,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
21282128
else {
21292129
val t2 = distributeAnd(tp2, tp1)
21302130
if (t2.exists) t2
2131-
else if (isErased) erasedGlb(tp1, tp2, isJava = false)
2131+
else if (isErased) erasedGlb(tp1, tp2)
21322132
else liftIfHK(tp1, tp2, op, original, _ | _)
21332133
// The ` | ` on variances is needed since variances are associated with bounds
21342134
// not lambdas. Example:

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

Lines changed: 87 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -394,32 +394,93 @@ object TypeErasure {
394394
}
395395

396396
/** The erased greatest lower bound of two erased type picks one of the two argument types.
397-
* It prefers, in this order:
398-
* - arrays over non-arrays
399-
* - subtypes over supertypes, unless isJava is set
400-
* - real classes over traits
397+
*
398+
* This operation has the following the properties:
399+
* - Associativity and commutativity, because this method acts as the minimum
400+
* of the total order induced by `compareErasedGlb`.
401+
* - Java compatibility: intersections that would be valid in Java code are
402+
* erased like javac would erase them (a Java intersection is composed of
403+
* exactly one class and one or more interfaces and always erases to the
404+
* class).
401405
*/
402-
def erasedGlb(tp1: Type, tp2: Type, isJava: Boolean)(using Context): Type = tp1 match {
403-
case JavaArrayType(elem1) =>
404-
tp2 match {
405-
case JavaArrayType(elem2) => JavaArrayType(erasedGlb(elem1, elem2, isJava))
406-
case _ => tp1
407-
}
408-
case _ =>
409-
tp2 match {
410-
case JavaArrayType(_) => tp2
411-
case _ =>
412-
val tsym1 = tp1.typeSymbol
413-
val tsym2 = tp2.typeSymbol
414-
if (!tsym2.exists) tp1
415-
else if (!tsym1.exists) tp2
416-
else if (!isJava && tsym1.derivesFrom(tsym2)) tp1
417-
else if (!isJava && tsym2.derivesFrom(tsym1)) tp2
418-
else if (tp1.typeSymbol.isRealClass) tp1
419-
else if (tp2.typeSymbol.isRealClass) tp2
420-
else tp1
421-
}
422-
}
406+
def erasedGlb(tp1: Type, tp2: Type)(using Context): Type =
407+
if compareErasedGlb(tp1, tp2) <= 0 then tp1 else tp2
408+
409+
/** Overload of `erasedGlb` to compare more than two types at once. */
410+
def erasedGlb(tps: List[Type])(using Context): Type =
411+
tps.min(using (a,b) => compareErasedGlb(a, b))
412+
413+
/** A comparison function that induces a total order on erased types,
414+
* where `A <= B` implies that the erasure of `A & B` should be A.
415+
*
416+
* This order respects the following properties:
417+
* - ErasedValueTypes <= non-ErasedValueTypes
418+
* - arrays <= non-arrays
419+
* - primitives <= non-primitives
420+
* - real classes <= traits
421+
* - subtypes <= supertypes
422+
*
423+
* Since this isn't enough to order to unrelated classes, we use
424+
* lexicographic ordering of the class symbol full name as a tie-breaker.
425+
* This ensure that `A <= B && B <= A` iff `A =:= B`.
426+
*
427+
* @see erasedGlb
428+
*/
429+
private def compareErasedGlb(tp1: Type, tp2: Type)(using Context): Int =
430+
// this check is purely an optimization.
431+
if tp1 eq tp2 then
432+
return 0
433+
434+
val isEVT1 = tp1.isInstanceOf[ErasedValueType]
435+
val isEVT2 = tp2.isInstanceOf[ErasedValueType]
436+
if isEVT1 && isEVT2 then
437+
return compareErasedGlb(tp1.asInstanceOf[ErasedValueType].tycon, tp2.asInstanceOf[ErasedValueType].tycon)
438+
else if isEVT1 then
439+
return -1
440+
else if isEVT2 then
441+
return 1
442+
443+
val isArray1 = tp1.isInstanceOf[JavaArrayType]
444+
val isArray2 = tp2.isInstanceOf[JavaArrayType]
445+
if isArray1 && isArray2 then
446+
return compareErasedGlb(tp1.asInstanceOf[JavaArrayType].elemType, tp2.asInstanceOf[JavaArrayType].elemType)
447+
else if isArray1 then
448+
return -1
449+
else if isArray2 then
450+
return 1
451+
452+
val sym1 = tp1.classSymbol
453+
val sym2 = tp2.classSymbol
454+
def compareClasses: Int =
455+
if sym1.isSubClass(sym2) then
456+
-1
457+
else if sym2.isSubClass(sym1) then
458+
1
459+
// Intentionally compare Strings and not Names, since the ordering on
460+
// Names depends on implementation details like `NameKind#tag`.
461+
else
462+
sym1.fullName.toString.compareTo(sym2.fullName.toString)
463+
464+
val isPrimitive1 = sym1.isPrimitiveValueClass
465+
val isPrimitive2 = sym2.isPrimitiveValueClass
466+
if isPrimitive1 && isPrimitive2 then
467+
return compareClasses
468+
else if isPrimitive1 then
469+
return -1
470+
else if isPrimitive2 then
471+
return 1
472+
473+
val isRealClass1 = sym1.isRealClass
474+
val isRealClass2 = sym2.isRealClass
475+
if isRealClass1 && isRealClass2 then
476+
return compareClasses
477+
else if isRealClass1 then
478+
return -1
479+
else if isRealClass2 then
480+
return 1
481+
482+
compareClasses
483+
end compareErasedGlb
423484

424485
/** Does the (possibly generic) type `tp` have the same erasure in all its
425486
* possible instantiations?
@@ -522,7 +583,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
522583
if sourceLanguage.isScala2 then
523584
this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp)))
524585
else
525-
erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava)
586+
erasedGlb(this(tp1), this(tp2))
526587
case OrType(tp1, tp2) =>
527588
if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then
528589
// In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were

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

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import core.Flags._
1010
import core.Names.{DerivedName, Name, SimpleName, TypeName}
1111
import core.Symbols._
1212
import core.TypeApplications.TypeParamInfo
13-
import core.TypeErasure.{erasure, isUnboundedGeneric}
13+
import core.TypeErasure.{erasedGlb, erasure, isUnboundedGeneric}
1414
import core.Types._
1515
import core.classfile.ClassfileConstants
1616
import ast.Trees._
@@ -19,6 +19,7 @@ import TypeUtils._
1919
import java.lang.StringBuilder
2020

2121
import scala.annotation.tailrec
22+
import scala.collection.mutable.ListBuffer
2223

2324
/** Helper object to generate generic java signatures, as defined in
2425
* the Java Virtual Machine Specification, §4.3.4
@@ -65,18 +66,79 @@ object GenericSignatures {
6566

6667
def boxedSig(tp: Type): Unit = jsig(tp.widenDealias, primitiveOK = false)
6768

69+
/** The signature of the upper-bound of a type parameter.
70+
*
71+
* @pre none of the bounds are themselves type parameters.
72+
* TODO: Remove this restriction so we can support things like:
73+
*
74+
* class Foo[A]:
75+
* def foo[S <: A & Object](...): ...
76+
*
77+
* Which should emit a signature `S <: A`. See the handling
78+
* of `AndType` in `jsig` which already supports `def foo(x: A & Object)`.
79+
*/
6880
def boundsSig(bounds: List[Type]): Unit = {
69-
val (isTrait, isClass) = bounds partition (_.typeSymbol.is(Trait))
70-
isClass match {
71-
case Nil => builder.append(':') // + boxedSig(ObjectTpe)
72-
case x :: _ => builder.append(':'); boxedSig(x)
73-
}
74-
isTrait.foreach { tp =>
81+
val (repr :: _, others) = splitIntersection(bounds)
82+
builder.append(':')
83+
84+
// According to the Java spec
85+
// (https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.4),
86+
// intersections erase to their first member and must start with a class.
87+
// So, if our intersection erases to a trait, in theory we should emit
88+
// just that trait in the generic signature even if the intersection type
89+
// is composed of multiple traits. But in practice Scala 2 has always
90+
// ignored this restriction as intersections of traits seem to be handled
91+
// correctly by javac, we do the same here since type soundness seems
92+
// more important than adhering to the spec.
93+
if repr.classSymbol.is(Trait) then
94+
builder.append(':')
95+
boxedSig(repr)
96+
// If we wanted to be compliant with the spec, we would `return` here.
97+
else
98+
boxedSig(repr)
99+
others.filter(_.classSymbol.is(Trait)).foreach { tp =>
75100
builder.append(':')
76101
boxedSig(tp)
77102
}
78103
}
79104

105+
/** The parents of this intersection where type parameters
106+
* that cannot appear in the signature have been replaced
107+
* by their upper-bound.
108+
*/
109+
def flattenedIntersection(tp: AndType)(using Context): List[Type] =
110+
val parents = ListBuffer[Type]()
111+
112+
def collect(parent: Type, parents: ListBuffer[Type]): Unit = parent.widenDealias match
113+
case AndType(tp1, tp2) =>
114+
collect(tp1, parents)
115+
collect(tp2, parents)
116+
case parent: TypeRef =>
117+
if parent.symbol.isClass || isTypeParameterInSig(parent.symbol, sym0) then
118+
parents += parent
119+
else
120+
collect(parent.superType, parents)
121+
case parent =>
122+
parents += parent
123+
124+
collect(tp, parents)
125+
parents.toList
126+
end flattenedIntersection
127+
128+
/** Split the `parents` of an intersection into two subsets:
129+
* those whose individual erasure matches the overall erasure
130+
* of the intersection and the others.
131+
*/
132+
def splitIntersection(parents: List[Type])(using Context): (List[Type], List[Type]) =
133+
val erasedParents = parents.map(erasure)
134+
val erasedCls = erasedGlb(erasedParents).classSymbol
135+
parents.zip(erasedParents)
136+
.partitionMap((parent, erasedParent) =>
137+
if erasedParent.classSymbol eq erasedCls then
138+
Left(parent)
139+
else
140+
Right(parent))
141+
80142
def paramSig(param: LambdaParam): Unit = {
81143
builder.append(sanitizeName(param.paramName))
82144
boundsSig(hiBounds(param.paramInfo.bounds))
@@ -263,8 +325,20 @@ object GenericSignatures {
263325
builder.append(')')
264326
methodResultSig(restpe)
265327

266-
case AndType(tp1, tp2) =>
267-
jsig(intersectionDominator(tp1 :: tp2 :: Nil), primitiveOK = primitiveOK)
328+
case tp: AndType =>
329+
// Only intersections appearing as the upper-bound of a type parameter
330+
// can be preserved in generic signatures and those are already
331+
// handled by `boundsSig`, so here we fallback to picking a parent of
332+
// the intersection to determine its overall signature. We must pick a
333+
// parent whose erasure matches the erasure of the intersection
334+
// because javac relies on the generic signature to determine the
335+
// bytecode signature. Additionally, we prefer picking a type
336+
// parameter since that will likely lead to a more precise type.
337+
val parents = flattenedIntersection(tp)
338+
val (reprParents, _) = splitIntersection(parents)
339+
val repr =
340+
reprParents.find(_.typeSymbol.is(TypeParam)).getOrElse(reprParents.head)
341+
jsig(repr, primitiveOK = primitiveOK)
268342

269343
case ci: ClassInfo =>
270344
def polyParamSig(tparams: List[TypeParamInfo]): Unit =
@@ -308,38 +382,6 @@ object GenericSignatures {
308382

309383
private class UnknownSig extends Exception
310384

311-
/** The intersection dominator (SLS 3.7) of a list of types is computed as follows.
312-
*
313-
* - If the list contains one or more occurrences of scala.Array with
314-
* type parameters El1, El2, ... then the dominator is scala.Array with
315-
* type parameter of intersectionDominator(List(El1, El2, ...)). <--- @PP: not yet in spec.
316-
* - Otherwise, the list is reduced to a subsequence containing only the
317-
* types that are not supertypes of other listed types (the span.)
318-
* - If the span is empty, the dominator is Object.
319-
* - If the span contains a class Tc which is not a trait and which is
320-
* not Object, the dominator is Tc. <--- @PP: "which is not Object" not in spec.
321-
* - Otherwise, the dominator is the first element of the span.
322-
*/
323-
private def intersectionDominator(parents: List[Type])(using Context): Type =
324-
if (parents.isEmpty) defn.ObjectType
325-
else {
326-
val psyms = parents map (_.typeSymbol)
327-
if (psyms contains defn.ArrayClass)
328-
// treat arrays specially
329-
defn.ArrayType.appliedTo(intersectionDominator(parents.filter(_.typeSymbol == defn.ArrayClass).map(t => t.argInfos.head)))
330-
else {
331-
// implement new spec for erasure of refined types.
332-
def isUnshadowed(psym: Symbol) =
333-
!(psyms exists (qsym => (psym ne qsym) && (qsym isSubClass psym)))
334-
val cs = parents.iterator.filter { p => // isUnshadowed is a bit expensive, so try classes first
335-
val psym = p.classSymbol
336-
psym.ensureCompleted()
337-
psym.isClass && !psym.is(Trait) && isUnshadowed(psym)
338-
}
339-
(if (cs.hasNext) cs else parents.iterator.filter(p => isUnshadowed(p.classSymbol))).next()
340-
}
341-
}
342-
343385
/* Drop redundant types (ones which are implemented by some other parent) from the immediate parents.
344386
* This is important on Android because there is otherwise an interface explosion.
345387
*/

0 commit comments

Comments
 (0)