Skip to content

Commit a149aed

Browse files
committed
Add double-defs checking for mixin forwarders
And demonstrate that the current scheme of generating mixin forwarders before erasure is broken: - Because they're generated before erasure, their erasure in the class where their defined might differ from the erasure of the method they forward to. This is normally OK since they'll get a bridge. - However, mixin forwarders can clash with other methods as demonstrated by neg/mixin-forwarder-clash1, but these clashes won't be detected under separate compilation since double-defs checks are only done based on the signature of methods where they're defined, so neg/mixin-forwarder-clash2 fails. Checking for clashes against the signatures of all possible forwarders would be really annoying and possibly expensive, so instead the next commit solves this issue by moving mixin forwarder generation after erasure.
1 parent 4e62ca9 commit a149aed

File tree

4 files changed

+82
-20
lines changed

4 files changed

+82
-20
lines changed

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

+9-20
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ object ElimErasedValueType {
2020
* of methods that now have the same signature but were not considered matching
2121
* before erasure.
2222
*/
23-
class ElimErasedValueType extends MiniPhase with InfoTransformer {
23+
class ElimErasedValueType extends MiniPhase with InfoTransformer { thisPhase =>
2424

2525
import tpd._
2626

@@ -75,11 +75,9 @@ class ElimErasedValueType extends MiniPhase with InfoTransformer {
7575

7676
/** Check that we don't have pairs of methods that override each other after
7777
* this phase, yet do not have matching types before erasure.
78-
* The before erasure test is performed after phase elimRepeated, so we
79-
* do not need to special case pairs of `T* / Seq[T]` parameters.
8078
*/
8179
private def checkNoClashes(root: Symbol)(implicit ctx: Context) = {
82-
val opc = new OverridingPairs.Cursor(root) {
80+
val opc = new OverridingPairs.Cursor(root)(ctx.withPhase(thisPhase)) {
8381
override def exclude(sym: Symbol) =
8482
!sym.is(Method) || sym.is(Bridge) || super.exclude(sym)
8583
override def matches(sym1: Symbol, sym2: Symbol) =
@@ -90,34 +88,25 @@ class ElimErasedValueType extends MiniPhase with InfoTransformer {
9088
val info1 = site.memberInfo(sym1)
9189
val info2 = site.memberInfo(sym2)
9290
def isDefined(sym: Symbol) = sym.originDenotation.validFor.firstPhaseId <= ctx.phaseId
93-
if (isDefined(sym1) && isDefined(sym2) && !info1.matchesLoosely(info2))
94-
// The reason for the `isDefined` condition is that we need to exclude mixin forwarders
95-
// from the tests. For instance, in compileStdLib, compiling scala.immutable.SetProxy, line 29:
96-
// new AbstractSet[B] with SetProxy[B] { val self = newSelf }
97-
// This generates two forwarders, one in AbstractSet, the other in the anonymous class itself.
98-
// Their signatures are:
99-
// method map: [B, That]
100-
// (f: B => B)(implicit bf: scala.collection.generic.CanBuildFrom[scala.collection.immutable.Set[B], B, That]): That override <method> <touched> in anonymous class scala.collection.AbstractSet[B] with scala.collection.immutable.SetProxy[B]{...} and
101-
// method map: [B, That](f: B => B)(implicit bf: scala.collection.generic.CanBuildFrom[scala.collection.Set[B], B, That]): That override <method> <touched> in class AbstractSet
102-
// These have same type after erasure:
103-
// (f: Function1, bf: scala.collection.generic.CanBuildFrom): Object
104-
//
105-
// The problem is that `map` was forwarded twice, with different instantiated types.
106-
// Maybe we should move mixin forwarding after erasure to avoid redundant forwarders like these.
91+
if (!info1.matchesLoosely(info2))
10792
ctx.error(DoubleDefinition(sym1, sym2, root), root.sourcePos)
10893
}
10994
val earlyCtx = ctx.withPhase(ctx.elimRepeatedPhase.next)
11095
while (opc.hasNext) {
11196
val sym1 = opc.overriding
11297
val sym2 = opc.overridden
98+
// Do the test at the earliest phase where both symbols existed.
99+
val phaseId = math.max(
100+
sym1.originDenotation.validFor.firstPhaseId,
101+
sym2.originDenotation.validFor.firstPhaseId)
113102
checkNoConflict(sym1, sym2, sym1.info)(earlyCtx)
114103
opc.next()
115104
}
116105
}
117106

118-
override def transformTypeDef(tree: TypeDef)(implicit ctx: Context): Tree = {
107+
override def prepareForTypeDef(tree: TypeDef)(implicit ctx: Context): Context = {
119108
checkNoClashes(tree.symbol)
120-
tree
109+
ctx
121110
}
122111

123112
override def transformInlined(tree: Inlined)(implicit ctx: Context): Tree =
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class Foo
2+
3+
// Using self-types to force mixin forwarders
4+
5+
trait OneA[X] {
6+
def concat(suffix: Int): X = ???
7+
}
8+
9+
trait OneB[X] { self: OneA[X] =>
10+
override def concat(suffix: Int): X = ???
11+
}
12+
13+
trait TwoA[Y <: Foo] {
14+
def concat[Dummy](suffix: Int): Y = ???
15+
}
16+
17+
trait TwoB[Y <: Foo] { self: TwoA[Y] =>
18+
override def concat[Dummy](suffix: Int): Y = ???
19+
}
20+
21+
class Bar1 extends OneA[Foo] with OneB[Foo]
22+
// Because mixin forwarders are generated before erasure, we get:
23+
// override def concat(suffix: Int): Foo
24+
25+
class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo] // error
26+
// We get a mixin forwarder for TwoB:
27+
// override def concat[Dummy](suffix: Int): Foo
28+
// which gets erased to:
29+
// override def concat(suffix: Int): Foo
30+
// This clashes with the forwarder generated in Bar1, and the compiler detects that:
31+
//
32+
// |class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo]
33+
// | ^
34+
// | Name clash between defined and inherited member:
35+
// | override def concat(suffix: Int): Foo in class Bar1 and
36+
// | override def concat: [Dummy](suffix: Int): Foo in class Bar2
37+
// | have the same type after erasure.
38+
//
39+
// But note that the compiler is able to see the mixin forwarder in Bar1
40+
// only because of joint compilation, this doesn't work with separate
41+
// compilation as in mixin-forwarder-clash2.
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class Foo
2+
3+
// Using self-types to force mixin forwarders
4+
5+
trait OneA[X] {
6+
def concat(suffix: Int): X = ???
7+
}
8+
9+
trait OneB[X] { self: OneA[X] =>
10+
override def concat(suffix: Int): X = ???
11+
}
12+
13+
trait TwoA[Y <: Foo] {
14+
def concat[Dummy](suffix: Int): Y = ???
15+
}
16+
17+
trait TwoB[Y <: Foo] { self: TwoA[Y] =>
18+
override def concat[Dummy](suffix: Int): Y = ???
19+
}
20+
21+
class Bar1 extends OneA[Foo] with OneB[Foo]
22+
// Because mixin forwarders are generated before erasure, we get:
23+
// override def concat(suffix: Int): Foo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo] // error
2+
// We get a mixin forwarder for TwoB:
3+
// override def concat[Dummy](suffix: Int): Foo
4+
// which gets erased to:
5+
// override def concat(suffix: Int): Foo
6+
// This clashes with the forwarder generated in Bar1, but
7+
// unlike with mixin-forwarder-clash1, the compiler
8+
// doesn't detect that due to separate compilation,
9+
// so this test case fails.

0 commit comments

Comments
 (0)