Skip to content

Commit 0fdb7e5

Browse files
authored
Merge pull request #6079 from dotty-staging/mixin-after-erasure-3
Move mixin forwarders generation after erasure
2 parents b537220 + 96cc754 commit 0fdb7e5

16 files changed

+222
-71
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class Compiler {
8282
new CollectNullableFields, // Collect fields that can be nulled out after use in lazy initialization
8383
new ElimOuterSelect, // Expand outer selections
8484
new AugmentScala2Traits, // Augments Scala2 traits with additional members needed for mixin composition.
85-
new ResolveSuper, // Implement super accessors and add forwarders to trait methods
85+
new ResolveSuper, // Implement super accessors
8686
new FunctionXXLForwarders, // Add forwarders for FunctionXXL apply method
8787
new ArrayConstructors) :: // Intercept creation of (non-generic) arrays and intrinsify.
8888
List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements.

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ object TypeErasure {
3636
private def erasureDependsOnArgs(sym: Symbol)(implicit ctx: Context) =
3737
sym == defn.ArrayClass || sym == defn.PairClass
3838

39+
def normalizeClass(cls: ClassSymbol)(implicit ctx: Context): ClassSymbol = {
40+
if (cls.owner == defn.ScalaPackageClass) {
41+
if (defn.specialErasure.contains(cls))
42+
return defn.specialErasure(cls)
43+
if (cls == defn.UnitClass)
44+
return defn.BoxedUnitClass
45+
}
46+
cls
47+
}
48+
3949
/** A predicate that tests whether a type is a legal erased type. Only asInstanceOf and
4050
* isInstanceOf may have types that do not satisfy the predicate.
4151
* ErasedValueType is considered an erased type because it is valid after Erasure (it is
@@ -530,16 +540,6 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
530540
this(tp)
531541
}
532542

533-
private def normalizeClass(cls: ClassSymbol)(implicit ctx: Context): ClassSymbol = {
534-
if (cls.owner == defn.ScalaPackageClass) {
535-
if (defn.specialErasure.contains(cls))
536-
return defn.specialErasure(cls)
537-
if (cls == defn.UnitClass)
538-
return defn.BoxedUnitClass
539-
}
540-
cls
541-
}
542-
543543
/** The name of the type as it is used in `Signature`s.
544544
* Need to ensure correspondence with erasure!
545545
*/

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ class AugmentScala2Traits extends MiniPhase with IdentityDenotTransformer { this
3939

4040
override def transformTemplate(impl: Template)(implicit ctx: Context): Template = {
4141
val cls = impl.symbol.owner.asClass
42-
for (mixin <- cls.mixins if mixin.is(Scala2x) && !mixin.is(Scala2xPartiallyAugmented))
43-
augmentScala2Trait(mixin)
42+
for (mixin <- cls.mixins) {
43+
val erasedMixin = TypeErasure.normalizeClass(mixin)
44+
if (erasedMixin.is(Scala2x) && !erasedMixin.is(Scala2xPartiallyAugmented))
45+
augmentScala2Trait(erasedMixin)
46+
}
4447
impl
4548
}
4649

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 =

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class Erasure extends Phase with DenotTransformer {
135135

136136
def assertErased(tp: Type, tree: tpd.Tree = tpd.EmptyTree)(implicit ctx: Context): Unit = {
137137
def isAllowed(cls: Symbol, sourceName: String) =
138-
tp.typeSymbol == cls && ctx.compilationUnit.source.file.name == sourceName
138+
tp.widen.typeSymbol == cls && ctx.compilationUnit.source.file.name == sourceName
139139
assert(isErasedType(tp) ||
140140
isAllowed(defn.ArrayClass, "Array.scala") ||
141141
isAllowed(defn.TupleClass, "Tuple.scala") ||

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ object Mixin {
8686
*
8787
* <mods> def x_=(y: T) = ()
8888
*
89+
* 3.5 (done in `mixinForwarders`) For every method
90+
* `<mods> def f[Ts](ps1)...(psN): U` imn M` that needs to be disambiguated:
91+
*
92+
* <mods> def f[Ts](ps1)...(psN): U = super[M].f[Ts](ps1)...(psN)
93+
*
94+
* A method in M needs to be disambiguated if it is concrete, not overridden in C,
95+
* and if it overrides another concrete method.
96+
*
8997
* 4. (done in `transformTemplate` and `transformSym`) Drop all parameters from trait
9098
* constructors.
9199
*
@@ -239,7 +247,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase =>
239247
else
240248
initial
241249
// transformFollowing call is needed to make memoize & lazy vals run
242-
transformFollowing(DefDef(implementation(getter.asTerm), rhs))
250+
transformFollowing(DefDef(mkForwarder(getter.asTerm), rhs))
243251
}
244252
else if (isScala2x || was(getter, ParamAccessor | Lazy)) EmptyTree
245253
else initial
@@ -248,7 +256,15 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase =>
248256

249257
def setters(mixin: ClassSymbol): List[Tree] =
250258
for (setter <- mixin.info.decls.filter(setr => setr.isSetter && !was(setr, Deferred)))
251-
yield transformFollowing(DefDef(implementation(setter.asTerm), unitLiteral.withSpan(cls.span)))
259+
yield transformFollowing(DefDef(mkForwarder(setter.asTerm), unitLiteral.withSpan(cls.span)))
260+
261+
def mixinForwarders(mixin: ClassSymbol): List[Tree] =
262+
for (meth <- mixin.info.decls.toList if needsForwarder(meth))
263+
yield {
264+
util.Stats.record("mixin forwarders")
265+
transformFollowing(polyDefDef(mkForwarder(meth.asTerm), forwarder(meth)))
266+
}
267+
252268

253269
cpy.Template(impl)(
254270
constr =
@@ -259,7 +275,7 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase =>
259275
if (cls is Trait) traitDefs(impl.body)
260276
else {
261277
val mixInits = mixins.flatMap { mixin =>
262-
flatten(traitInits(mixin)) ::: superCallOpt(mixin) ::: setters(mixin)
278+
flatten(traitInits(mixin)) ::: superCallOpt(mixin) ::: setters(mixin) ::: mixinForwarders(mixin)
263279
}
264280
superCallOpt(superCls) ::: mixInits ::: impl.body
265281
})

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class MixinOps(cls: ClassSymbol, thisPhase: DenotTransformer)(implicit ctx: Cont
1818
map(n => ctx.getClassIfDefined("org.junit." + n)).
1919
filter(_.exists)
2020

21-
def implementation(member: TermSymbol): TermSymbol = {
21+
def mkForwarder(member: TermSymbol): TermSymbol = {
2222
val res = member.copy(
2323
owner = cls,
2424
name = member.name.stripScala2LocalSuffix,
@@ -45,12 +45,6 @@ class MixinOps(cls: ClassSymbol, thisPhase: DenotTransformer)(implicit ctx: Cont
4545
def isCurrent(sym: Symbol): Boolean =
4646
ctx.atPhase(thisPhase) { implicit ctx =>
4747
cls.info.nonPrivateMember(sym.name).hasAltWith(_.symbol == sym)
48-
// this is a hot spot, where we spend several seconds while compiling stdlib
49-
// unfortunately it will discard and recompute all the member chaches,
50-
// both making itself slow and slowing down anything that runs after it
51-
// because resolveSuper uses hacks with explicit adding to scopes through .enter
52-
// this cannot be fixed by a smarter caching strategy. With current implementation
53-
// we HAVE to discard caches here for correctness
5448
}
5549

5650
/** Does `method` need a forwarder to in class `cls`

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

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,16 @@ import NameKinds._
1414
import ResolveSuper._
1515
import reporting.diagnostic.messages.IllegalSuperAccessor
1616

17-
/** This phase adds super accessors and method overrides where
18-
* linearization differs from Java's rule for default methods in interfaces.
19-
* In particular:
17+
/** This phase implements super accessors in classes that need them.
2018
*
21-
* For every trait M directly implemented by the class (see SymUtils.mixin), in
22-
* reverse linearization order, add the following definitions to C:
19+
* For every trait M directly implemented by the class (see SymUtils.mixin), in
20+
* reverse linearization order, add the following definitions to C:
2321
*
24-
* 3.1 (done in `superAccessors`) For every superAccessor
25-
* `<mods> def super$f[Ts](ps1)...(psN): U` in M:
22+
* For every superAccessor `<mods> def super$f[Ts](ps1)...(psN): U` in M:
2623
*
27-
* <mods> def super$f[Ts](ps1)...(psN): U = super[S].f[Ts](ps1)...(psN)
24+
* <mods> def super$f[Ts](ps1)...(psN): U = super[S].f[Ts](ps1)...(psN)
2825
*
29-
* where `S` is the superclass of `M` in the linearization of `C`.
30-
*
31-
* 3.2 (done in `methodOverrides`) For every method
32-
* `<mods> def f[Ts](ps1)...(psN): U` in M` that needs to be disambiguated:
33-
*
34-
* <mods> def f[Ts](ps1)...(psN): U = super[M].f[Ts](ps1)...(psN)
35-
*
36-
* A method in M needs to be disambiguated if it is concrete, not overridden in C,
37-
* and if it overrides another concrete method.
26+
* where `S` is the superclass of `M` in the linearization of `C`.
3827
*
3928
* This is the first part of what was the mixin phase. It is complemented by
4029
* Mixin, which runs after erasure.
@@ -59,17 +48,10 @@ class ResolveSuper extends MiniPhase with IdentityDenotTransformer { thisPhase =
5948
for (superAcc <- mixin.info.decls.filter(_.isSuperAccessor))
6049
yield {
6150
util.Stats.record("super accessors")
62-
polyDefDef(implementation(superAcc.asTerm), forwarder(rebindSuper(cls, superAcc)))
63-
}
64-
65-
def methodOverrides(mixin: ClassSymbol): List[Tree] =
66-
for (meth <- mixin.info.decls.toList if needsForwarder(meth))
67-
yield {
68-
util.Stats.record("method forwarders")
69-
polyDefDef(implementation(meth.asTerm), forwarder(meth))
51+
polyDefDef(mkForwarder(superAcc.asTerm), forwarder(rebindSuper(cls, superAcc)))
7052
}
7153

72-
val overrides = mixins.flatMap(mixin => superAccessors(mixin) ::: methodOverrides(mixin))
54+
val overrides = mixins.flatMap(superAccessors)
7355

7456
cpy.Template(impl)(body = overrides ::: impl.body)
7557
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<527..527> in mixin-forwarder-clash1.scala
2+
Name clash between inherited members:
3+
override def concat(suffix: Int): X in trait OneB at line 10 and
4+
override def concat: [Dummy](suffix: Int): Y in trait TwoB at line 18
5+
have the same type after erasure.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 after erasure, we get:
23+
// override def concat(suffix: Int): Object
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(suffix: Int): Object
28+
// This clashes with the forwarder generated in Bar1, and the compiler detects that:
29+
//
30+
// |class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo]
31+
// | ^
32+
// | Name clash between inherited members:
33+
// | override def concat(suffix: Int): Object in trait OneB at line 10 and
34+
// | override def concat: [Dummy](suffix: Int): Y in trait TwoB at line 18
35+
// | have the same type after erasure.
36+
//
37+
// This also works with separate compilation as demonstrated by
38+
// mixin-forwarder-clash2.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<6..6> in B_2.scala
2+
Name clash between inherited members:
3+
override def concat(suffix: Int): X in trait OneB and
4+
override def concat: [Dummy](suffix: Int): Y in trait TwoB
5+
have the same type after erasure.
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 after erasure, we get:
23+
// override def concat(suffix: Int): Object
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo] // error
2+
// We get a mixin forwarder for TwoB:
3+
// override def concat(suffix: Int): Object
4+
// This clashes with the forwarder generated in Bar1, and
5+
// we can detect that even with separate compilation,
6+
// because forwarders are always generated after erasure
7+
// so their signature matches the erased signature of the
8+
// method they forward to, which double-defs check will
9+
// consider:
10+
//
11+
// |class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo]
12+
// | ^
13+
// | Name clash between inherited members:
14+
// | override def concat(suffix: Int): X in trait OneB and
15+
// | override def concat: [Dummy](suffix: Int): Y in trait TwoB
16+
// | have the same type after erasure.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This test case used to fail when mixin forwarders were generated before erasure,
2+
// it doesn't anymore since the forwarders generated after erasure do not clash,
3+
// the comments are preserved for posterity.
4+
5+
class Foo
6+
7+
// Using self-types to force mixin forwarders
8+
9+
trait OneA[X] {
10+
def concat(suffix: Int): X = ???
11+
}
12+
13+
trait OneB[X] { self: OneA[X] =>
14+
override def concat(suffix: Int): X = ???
15+
}
16+
17+
trait TwoA[Y <: Foo] {
18+
def concat[Dummy](suffix: Int): Y = ???
19+
}
20+
21+
trait TwoB[Y <: Foo] { self: TwoA[Y] =>
22+
override def concat[Dummy](suffix: Int): Y = ???
23+
}
24+
25+
class Bar1 extends OneA[Foo] with OneB[Foo]
26+
// Because mixin forwarders are generated before erasure, we get:
27+
// override def concat(suffix: Int): Foo
28+
29+
class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo] // error
30+
// We get a mixin forwarder for TwoB:
31+
// override def concat[Dummy](suffix: Int): Foo
32+
// which gets erased to:
33+
// override def concat(suffix: Int): Foo
34+
// This clashes with the forwarder generated in Bar1, and the compiler detects that:
35+
//
36+
// |class Bar2 extends Bar1 with TwoA[Foo] with TwoB[Foo]
37+
// | ^
38+
// | Name clash between defined and inherited member:
39+
// | override def concat(suffix: Int): Foo in class Bar1 and
40+
// | override def concat: [Dummy](suffix: Int): Foo in class Bar2
41+
// | have the same type after erasure.
42+
//
43+
// But note that the compiler is able to see the mixin forwarder in Bar1
44+
// only because of joint compilation, this doesn't work with separate
45+
// compilation as in mixin-forwarder-clash2.

0 commit comments

Comments
 (0)