Skip to content

Commit 423e2f9

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. 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 2e05d9e commit 423e2f9

File tree

12 files changed

+239
-59
lines changed

12 files changed

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

424474
/** Does the (possibly generic) type `tp` have the same erasure in all its
425475
* possible instantiations?
@@ -522,7 +572,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
522572
if sourceLanguage.isScala2 then
523573
this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp)))
524574
else
525-
erasedGlb(this(tp1), this(tp2), isJava = sourceLanguage.isJava)
575+
erasedGlb(this(tp1), this(tp2))
526576
case OrType(tp1, tp2) =>
527577
if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then
528578
// In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were

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

Lines changed: 83 additions & 9 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)
268342

269343
case ci: ClassInfo =>
270344
def polyParamSig(tparams: List[TypeParamInfo]): Unit =

sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Outer {
2222
// This is enforced by dottyApp/Main.scala
2323
class Z {
2424
def a_01(a: A with B): Unit = {}
25-
def b_02X(b: B with A): Unit = {}
25+
def a_02X(b: B with A): Unit = {}
2626
def a_02(a: A with B with A): Unit = {}
2727
def a_03(a: A with (B with A)): Unit = {}
2828
def a_04(b: A with (B with A) @foo): Unit = {}
@@ -33,15 +33,15 @@ class Z {
3333
def a_06(a: T1): Unit = {}
3434

3535
type S <: B with T1
36-
def b_07(a: S): Unit = {}
36+
def a_07(a: S): Unit = {}
3737

3838
type T2 <: B with A
3939
type U <: T2 with S
40-
def b_08(b: U): Unit = {}
40+
def a_08(b: U): Unit = {}
4141

4242
val singB: B = new B {}
4343
def a_09(a: A with singB.type): Unit = {}
44-
def b_10(b: singB.type with A): Unit = {}
44+
def a_10(b: singB.type with A): Unit = {}
4545

4646
type V >: SubB <: B
4747
def b_11(b: V): Unit = {}
@@ -71,46 +71,46 @@ class Z {
7171

7272
type W1 <: A with Cov[Any]
7373
type X1 <: Cov[Int] with W1
74-
def cov_21(a: X1): Unit = {}
74+
def a_21(a: X1): Unit = {}
7575

7676
type W2 <: A with Cov[Any]
7777
type X2 <: Cov[Int] with W2
78-
def cov_22(a: X2): Unit = {}
78+
def a_22(a: X2): Unit = {}
7979

8080
def z_23(z: A with this.type): Unit = {}
8181
def z_24(z: this.type with A): Unit = {}
8282

8383
def a_25(b: A with (B { type T })): Unit = {}
8484
def a_26(a: (A { type T }) with ((B with A) { type T })): Unit = {}
8585

86-
def b_27(a: VC with B): Unit = {}
87-
def b_28(a: B with VC): Unit = {}
86+
def a_27(a: VC with B): Unit = {}
87+
def a_28(a: B with VC): Unit = {}
8888

8989
val o1: Outer = new Outer
9090
val o2: Outer = new Outer
91-
def f_29(f: o1.E with o1.F): Unit = {}
92-
def f_30(f: o1.F with o1.E): Unit = {}
93-
def f_31(f: o1.E with o2.F): Unit = {}
94-
def f_32(f: o2.F with o1.E): Unit = {}
95-
def f_33(f: Outer#E with Outer#F): Unit = {}
96-
def f_34(f: Outer#F with Outer#E): Unit = {}
91+
def e_29(f: o1.E with o1.F): Unit = {}
92+
def e_30(f: o1.F with o1.E): Unit = {}
93+
def e_31(f: o1.E with o2.F): Unit = {}
94+
def e_32(f: o2.F with o1.E): Unit = {}
95+
def e_33(f: Outer#E with Outer#F): Unit = {}
96+
def e_34(f: Outer#F with Outer#E): Unit = {}
9797

9898
val structural1: { type DSub <: D } = new { type DSub <: D }
9999
def d_35(a: A with structural1.DSub): Unit = {}
100100
def d_36(a: structural1.DSub with A): Unit = {}
101-
def z_37(z: Z with structural1.DSub): Unit = {}
101+
def d_37(z: Z with structural1.DSub): Unit = {}
102102
def d_38(z: structural1.DSub with Z): Unit = {}
103103

104104
val structural2: { type SubCB <: C with B } = new { type SubCB <: C with B }
105-
def c_39(c: structural2.SubCB with B): Unit = {}
105+
def b_39(c: structural2.SubCB with B): Unit = {}
106106
def b_40(c: B with structural2.SubCB): Unit = {}
107107

108108
val structural3a: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB }
109109
val structural3b: { type SubB <: B; type SubCB <: C with SubB } = new { type SubB <: B; type SubCB <: C with SubB }
110110
def b_41(c: structural3a.SubB with structural3a.SubCB): Unit = {}
111-
def c_42(c: structural3a.SubCB with structural3a.SubB): Unit = {}
111+
def b_42(c: structural3a.SubCB with structural3a.SubB): Unit = {}
112112
def b_43(b: structural3a.SubB with structural3b.SubCB): Unit = {}
113-
def c_44(c: structural3b.SubCB with structural3a.SubB): Unit = {}
113+
def b_44(c: structural3b.SubCB with structural3a.SubB): Unit = {}
114114

115115
type SubStructural <: C with structural3a.SubB
116116
def b_45(x: structural3a.SubB with SubStructural): Unit = {}

tests/neg/i5139.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
trait A
2+
trait B
3+
4+
class Overload {
5+
def m(ab: A&B) = ab
6+
def m(ab: B&A) = ab // error: double definition
7+
}

tests/neg/ostermann.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
trait A { def foo(a: A) : Unit }
2+
3+
trait C extends A {
4+
override def foo(a: A with Any): Unit // error: method foo has a different signature than the overridden declaration
5+
}

tests/pos/t8300-overloading.scala renamed to tests/neg/t8300-overloading.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// cf. neg/t8300-overloading.scala
21
trait Universe {
32
type Name >: Null <: AnyRef with NameApi
43
trait NameApi
@@ -12,5 +11,5 @@ object Test extends App {
1211
import u.*
1312

1413
def foo(name: Name) = ???
15-
def foo(name: TermName) = ???
14+
def foo(name: TermName) = ??? // error: double definition, same type after erasure
1615
}

tests/pos/i5139.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
trait A
2+
trait B
3+
4+
class Base{
5+
def m(ab: A&B) = ab
6+
}
7+
class Derived extends Base{
8+
override def m(ab: B&A) = ab // unexpected error
9+
}

tests/pos/ostermann.scala

Lines changed: 0 additions & 3 deletions
This file was deleted.

tests/run/t12300/A_1.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
trait One
2+
trait Two
3+
4+
class X
5+
class Y extends X
6+
7+
trait YSub extends Y
8+
9+
class A {
10+
def foo1[T <: Object with One](x: T): Unit = {}
11+
def foo2[T <: One with Object](x: T): Unit = {}
12+
def foo3[T <: One with Two](x: T): Unit = {}
13+
def foo4[T <: Two with One](x: T): Unit = {}
14+
def foo5[T <: X with Y](x: T): Unit = {}
15+
def foo6[T <: Y with X](x: T): Unit = {}
16+
def foo7[T <: X with YSub](x: T): Unit = {}
17+
def foo8[T <: YSub with X](x: T): Unit = {}
18+
}

0 commit comments

Comments
 (0)