Skip to content

Commit 147df0d

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 b537220 commit 147df0d

File tree

4 files changed

+81
-21
lines changed

4 files changed

+81
-21
lines changed

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

Lines changed: 8 additions & 21 deletions
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) =
@@ -89,35 +87,24 @@ class ElimErasedValueType extends MiniPhase with InfoTransformer {
8987
val site = root.thisType
9088
val info1 = site.memberInfo(sym1)
9189
val info2 = site.memberInfo(sym2)
92-
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.
90+
if (!info1.matchesLoosely(info2))
10791
ctx.error(DoubleDefinition(sym1, sym2, root), root.sourcePos)
10892
}
10993
val earlyCtx = ctx.withPhase(ctx.elimRepeatedPhase.next)
11094
while (opc.hasNext) {
11195
val sym1 = opc.overriding
11296
val sym2 = opc.overridden
97+
// Do the test at the earliest phase where both symbols existed.
98+
val phaseId =
99+
sym1.originDenotation.validFor.firstPhaseId max sym2.originDenotation.validFor.firstPhaseId
113100
checkNoConflict(sym1, sym2, sym1.info)(earlyCtx)
114101
opc.next()
115102
}
116103
}
117104

118-
override def transformTypeDef(tree: TypeDef)(implicit ctx: Context): Tree = {
105+
override def prepareForTypeDef(tree: TypeDef)(implicit ctx: Context): Context = {
119106
checkNoClashes(tree.symbol)
120-
tree
107+
ctx
121108
}
122109

123110
override def transformInlined(tree: Inlined)(implicit ctx: Context): Tree =
Lines changed: 41 additions & 0 deletions
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.
Lines changed: 23 additions & 0 deletions
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
Lines changed: 9 additions & 0 deletions
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)