Skip to content

Commit 9af4bad

Browse files
committed
Associative, commutative, Java-friendly intersection erasure
Thanks to #11603, we can now freely decide how we erase our intersection types without compromising compatibility with Scala 2. We take advantage of this by designing a new erasedGlb algorithm which respects associativity and commutativity and can also erase Java intersections without any special case. Incidentally, this lets us reverse a recent change in scala-stm which was previously required due to two equivalent types ending up with different signatures. This commit also improves the way we handle intersections in Java generic signature to produce more precise types when possible and to ensure that we never emit a type that would lead to javac emitting invalid bytecode (which can happen with Scala 2: scala/bug#12300). Fixes #5139.
1 parent 0195dff commit 9af4bad

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)