Skip to content

Commit 5717548

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 5717548

File tree

15 files changed

+270
-92
lines changed

15 files changed

+270
-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: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -394,32 +394,92 @@ 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 act 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+
* Additionally, lexicographic ordering of the class symbol full name is used
424+
* as a tie-breaker so `A <= B && B <= A` iff `A =:= B`.
425+
*
426+
* @see erasedGlb
427+
*/
428+
private def compareErasedGlb(tp1: Type, tp2: Type)(using Context): Int =
429+
// this check is purely an optimization.
430+
if tp1 eq tp2 then
431+
return 0
432+
433+
val isEVT1 = tp1.isInstanceOf[ErasedValueType]
434+
val isEVT2 = tp2.isInstanceOf[ErasedValueType]
435+
if isEVT1 && isEVT2 then
436+
return compareErasedGlb(tp1.asInstanceOf[ErasedValueType].tycon, tp2.asInstanceOf[ErasedValueType].tycon)
437+
else if isEVT1 then
438+
return -1
439+
else if isEVT2 then
440+
return 1
441+
442+
val isArray1 = tp1.isInstanceOf[JavaArrayType]
443+
val isArray2 = tp2.isInstanceOf[JavaArrayType]
444+
if isArray1 && isArray2 then
445+
return compareErasedGlb(tp1.asInstanceOf[JavaArrayType].elemType, tp2.asInstanceOf[JavaArrayType].elemType)
446+
else if isArray1 then
447+
return -1
448+
else if isArray2 then
449+
return 1
450+
451+
val sym1 = tp1.classSymbol
452+
val sym2 = tp2.classSymbol
453+
def compareClasses: Int =
454+
if sym1.isSubClass(sym2) then
455+
-1
456+
else if sym2.isSubClass(sym1) then
457+
1
458+
// Intentionally compare Strings and not Names, since the ordering on
459+
// Names depends on implementation details like `NameKind#tag`.
460+
else
461+
sym1.fullName.toString.compareTo(sym2.fullName.toString)
462+
463+
val isPrimitive1 = sym1.isPrimitiveValueClass
464+
val isPrimitive2 = sym2.isPrimitiveValueClass
465+
if isPrimitive1 && isPrimitive2 then
466+
return compareClasses
467+
else if isPrimitive1 then
468+
return -1
469+
else if isPrimitive2 then
470+
return 1
471+
472+
val isRealClass1 = sym1.isRealClass
473+
val isRealClass2 = sym2.isRealClass
474+
if isRealClass1 && isRealClass2 then
475+
return compareClasses
476+
else if isRealClass1 then
477+
return -1
478+
else if isRealClass2 then
479+
return 1
480+
481+
compareClasses
482+
end compareErasedGlb
423483

424484
/** Does the (possibly generic) type `tp` have the same erasure in all its
425485
* possible instantiations?
@@ -522,7 +582,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
522582
if sourceLanguage.isScala2 then
523583
this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp)))
524584
else
525-
erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava)
585+
erasedGlb(this(tp1), this(tp2))
526586
case OrType(tp1, tp2) =>
527587
if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then
528588
// 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)