Skip to content

Commit 6da367b

Browse files
authored
Merge pull request #8253 from dotty-staging/fix-#8033
Fix #8033: Eliminate unnecessary outer accessors
2 parents 19fd9d4 + 363985e commit 6da367b

File tree

10 files changed

+245
-26
lines changed

10 files changed

+245
-26
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,11 @@ class Compiler {
110110
new GetClass) :: // Rewrites getClass calls on primitive types.
111111
List(new LinkScala2Impls, // Redirect calls to trait methods defined by Scala 2.x, so that they now go to
112112
new LambdaLift, // Lifts out nested functions to class scope, storing free variables in environments
113-
// Note: in this mini-phase block scopes are incorrect. No phases that rely on scopes should be here
114-
new ElimStaticThis) :: // Replace `this` references to static objects by global identifiers
115-
List(new Flatten, // Lift all inner classes to package scope
113+
// Note: in this mini-phase block scopes are incorrect. No phases that rely on scopes should be here
114+
new ElimStaticThis, // Replace `this` references to static objects by global identifiers
115+
new CountOuterAccesses) :: // Identify outer accessors that can be dropped
116+
List(new DropOuterAccessors, // Drop unused outer accessors
117+
new Flatten, // Lift all inner classes to package scope
116118
new RenameLifted, // Renames lifted classes to local numbering scheme
117119
new TransformWildcards, // Replace wildcards with default values
118120
new MoveStatics, // Move static methods from companion to the class itself

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ object Phases {
230230
private var myErasurePhase: Phase = _
231231
private var myElimErasedValueTypePhase: Phase = _
232232
private var myLambdaLiftPhase: Phase = _
233+
private var myCountOuterAccessesPhase: Phase = _
233234
private var myFlattenPhase: Phase = _
234235
private var myGenBCodePhase: Phase = _
235236

@@ -248,6 +249,7 @@ object Phases {
248249
final def erasurePhase: Phase = myErasurePhase
249250
final def elimErasedValueTypePhase: Phase = myElimErasedValueTypePhase
250251
final def lambdaLiftPhase: Phase = myLambdaLiftPhase
252+
final def countOuterAccessesPhase = myCountOuterAccessesPhase
251253
final def flattenPhase: Phase = myFlattenPhase
252254
final def genBCodePhase: Phase = myGenBCodePhase
253255

@@ -267,6 +269,7 @@ object Phases {
267269
myElimErasedValueTypePhase = phaseOfClass(classOf[ElimErasedValueType])
268270
myPatmatPhase = phaseOfClass(classOf[PatternMatcher])
269271
myLambdaLiftPhase = phaseOfClass(classOf[LambdaLift])
272+
myCountOuterAccessesPhase = phaseOfClass(classOf[CountOuterAccesses])
270273
myFlattenPhase = phaseOfClass(classOf[Flatten])
271274
myExplicitOuterPhase = phaseOfClass(classOf[ExplicitOuter])
272275
myGettersPhase = phaseOfClass(classOf[Getters])

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,9 +1079,15 @@ object SymDenotations {
10791079
final def lexicallyEnclosingClass(implicit ctx: Context): Symbol =
10801080
if (!exists || isClass) symbol else owner.lexicallyEnclosingClass
10811081

1082+
/** A class is extensible if it is not final, nor a module class,
1083+
* nor an anonymous class.
1084+
*/
1085+
final def isExtensibleClass(using Context): Boolean =
1086+
isClass && !isOneOf(FinalOrModuleClass) && !isAnonymousClass
1087+
10821088
/** A symbol is effectively final if it cannot be overridden in a subclass */
10831089
final def isEffectivelyFinal(implicit ctx: Context): Boolean =
1084-
isOneOf(EffectivelyFinalFlags) || !owner.isClass || owner.isOneOf(FinalOrModuleClass) || owner.isAnonymousClass
1090+
isOneOf(EffectivelyFinalFlags) || !owner.isExtensibleClass
10851091

10861092
/** A class is effectively sealed if has the `final` or `sealed` modifier, or it
10871093
* is defined in Scala 3 and is neither abstract nor open.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
177177
) &&
178178
fn.symbol.info.resultType.classSymbol == outerParam.info.classSymbol =>
179179
ref(outerParam)
180+
case tree: RefTree if tree.symbol.is(ParamAccessor) && tree.symbol.name == nme.OUTER =>
181+
ref(outerParam)
180182
case _ =>
181183
super.transform(tree)
182184
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package dotty.tools.dotc
2+
package transform
3+
4+
import core._
5+
import MegaPhase.MiniPhase
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import ast._
8+
import Trees._
9+
import Flags._
10+
import Symbols._
11+
import Decorators._
12+
import ExplicitOuter.isOuterParamAccessor
13+
14+
import collection.mutable
15+
16+
object CountOuterAccesses:
17+
val name: String = "countOuterAccesses"
18+
19+
/** Characterizes outer accessors and outer fields that can be dropped
20+
* if there are no references to them from within the toplevel class
21+
* where they are defined.
22+
*/
23+
def mightBeDropped(sym: Symbol)(using Context) =
24+
def isLocal(cls: Symbol) =
25+
cls.isAnonymousClass
26+
|| cls.owner.isTerm
27+
|| cls.accessBoundary(defn.RootClass).isContainedIn(cls.topLevelClass)
28+
(sym.is(OuterAccessor) || sym.isOuterParamAccessor) && isLocal(sym.owner)
29+
30+
/** Counts number of accesses to outer accessors and outer fields of
31+
* classes that are visible only within one source file. The info
32+
* is collected in `outerAccessCount` and used in the subsequent
33+
* DropOuterAccessors phase
34+
*/
35+
class CountOuterAccesses extends MiniPhase:
36+
thisPhase =>
37+
import tpd._
38+
39+
override def phaseName: String = CountOuterAccesses.name
40+
41+
override def runsAfter: Set[String] = Set(LambdaLift.name)
42+
// LambdaLift can create outer paths. These need to be known in this phase.
43+
44+
/** The number of times an outer accessor that might be dropped is accessed */
45+
val outerAccessCount = new mutable.HashMap[Symbol, Int] {
46+
override def default(s: Symbol): Int = 0
47+
}
48+
49+
private def markAccessed(tree: RefTree)(implicit ctx: Context): Tree =
50+
val sym = tree.symbol
51+
if CountOuterAccesses.mightBeDropped(sym) then outerAccessCount(sym) += 1
52+
tree
53+
54+
override def transformIdent(tree: Ident)(using Context): Tree =
55+
markAccessed(tree)
56+
57+
override def transformSelect(tree: Select)(using Context): Tree =
58+
markAccessed(tree)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package dotty.tools.dotc
2+
package transform
3+
4+
import core._
5+
import MegaPhase.MiniPhase
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import ast._
8+
import Trees._
9+
import Flags._
10+
import Symbols._
11+
import Contexts._
12+
import Decorators._
13+
import DenotTransformers._
14+
import ExplicitOuter.isOuterParamAccessor
15+
import CountOuterAccesses.mightBeDropped
16+
import collection.mutable
17+
import annotation.threadUnsafe
18+
19+
object DropOuterAccessors:
20+
val name: String = "dropOuterAccessors"
21+
22+
/** Drops unused outer accessors of inner classes that are visible only in one
23+
* toplevel class. For other classes, we can't tell whether an outer accessor
24+
* is used or not. It could for instance be used in a type test in some other source.
25+
*/
26+
class DropOuterAccessors extends MiniPhase with IdentityDenotTransformer:
27+
thisPhase =>
28+
import tpd._
29+
30+
override def phaseName: String = DropOuterAccessors.name
31+
32+
override def runsAfterGroupsOf: Set[String] = Set(CountOuterAccesses.name)
33+
34+
override def changesMembers: Boolean = true // the phase drops outer accessors
35+
36+
override def transformTemplate(impl: Template)(using ctx: Context): Tree =
37+
val outerAccessCount = ctx.base.countOuterAccessesPhase
38+
.asInstanceOf[CountOuterAccesses]
39+
.outerAccessCount
40+
41+
def dropOuterAccessor(stat: Tree): Boolean = stat match
42+
case stat: DefDef
43+
if stat.symbol.is(OuterAccessor)
44+
&& mightBeDropped(stat.symbol)
45+
&& outerAccessCount(stat.symbol) == 0 =>
46+
assert(stat.rhs.isInstanceOf[RefTree], stat)
47+
assert(outerAccessCount(stat.rhs.symbol) > 0)
48+
outerAccessCount(stat.rhs.symbol) -= 1
49+
stat.symbol.dropAfter(thisPhase)
50+
true
51+
case _ =>
52+
false
53+
54+
val droppedParamAccessors = mutable.Set[Symbol]()
55+
56+
def dropOuterParamAccessor(stat: Tree): Boolean = stat match
57+
case stat: ValDef
58+
if stat.symbol.isOuterParamAccessor
59+
&& mightBeDropped(stat.symbol)
60+
&& outerAccessCount(stat.symbol) == 1 =>
61+
droppedParamAccessors += stat.symbol
62+
stat.symbol.dropAfter(thisPhase)
63+
true
64+
case _ =>
65+
false
66+
67+
def dropOuterInit(stat: Tree): Boolean = stat match
68+
case Assign(lhs, rhs) => droppedParamAccessors.remove(lhs.symbol)
69+
case _ => false
70+
71+
val body1 = impl.body
72+
.filterNot(dropOuterAccessor)
73+
.filterNot(dropOuterParamAccessor)
74+
val constr1 =
75+
if droppedParamAccessors.isEmpty then impl.constr
76+
else cpy.DefDef(impl.constr)(
77+
rhs = impl.constr.rhs match {
78+
case rhs @ Block(inits, expr) =>
79+
cpy.Block(rhs)(inits.filterNot(dropOuterInit), expr)
80+
})
81+
assert(droppedParamAccessors.isEmpty,
82+
i"""Failed to eliminate: $droppedParamAccessors
83+
when dropping outer accessors for ${ctx.owner} with
84+
$impl""")
85+
cpy.Template(impl)(constr = constr1, body = body1)
86+
end transformTemplate

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
package dotty.tools.dotc
1+
package dotty.tools
2+
package dotc
23
package transform
34

45
import MegaPhase._
@@ -178,7 +179,7 @@ object ExplicitOuter {
178179

179180
/** A new param accessor for the outer field in class `cls` */
180181
private def newOuterParamAccessor(cls: ClassSymbol)(implicit ctx: Context) =
181-
newOuterSym(cls, cls, nme.OUTER, Private | ParamAccessor)
182+
newOuterSym(cls, cls, nme.OUTER, Private | Local | ParamAccessor)
182183

183184
/** A new outer accessor for class `cls` which is a member of `owner` */
184185
private def newOuterAccessor(owner: ClassSymbol, cls: ClassSymbol)(implicit ctx: Context) = {
@@ -322,6 +323,9 @@ object ExplicitOuter {
322323
tpe
323324
}
324325

326+
def (sym: Symbol).isOuterParamAccessor(using Context): Boolean =
327+
sym.is(ParamAccessor) && sym.name == nme.OUTER
328+
325329
def outer(implicit ctx: Context): OuterOps = new OuterOps(ctx)
326330

327331
/** The operations in this class
@@ -386,26 +390,28 @@ object ExplicitOuter {
386390
*/
387391
def path(start: Tree = This(ctx.owner.lexicallyEnclosingClass.asClass),
388392
toCls: Symbol = NoSymbol,
389-
count: Int = -1): Tree = try {
390-
@tailrec def loop(tree: Tree, count: Int): Tree = {
391-
val treeCls = tree.tpe.widen.classSymbol
392-
val outerAccessorCtx = ctx.withPhaseNoLater(ctx.lambdaLiftPhase) // lambdalift mangles local class names, which means we cannot reliably find outer acessors anymore
393-
ctx.log(i"outer to $toCls of $tree: ${tree.tpe}, looking for ${outerAccName(treeCls.asClass)(outerAccessorCtx)} in $treeCls")
394-
if (count == 0 || count < 0 && treeCls == toCls) tree
395-
else {
396-
val acc = outerAccessor(treeCls.asClass)(outerAccessorCtx)
397-
assert(acc.exists,
398-
i"failure to construct path from ${ctx.owner.ownersIterator.toList}%/% to `this` of ${toCls.showLocated};\n${treeCls.showLocated} does not have an outer accessor")
399-
loop(tree.select(acc).ensureApplied, count - 1)
400-
}
401-
}
402-
ctx.log(i"computing outerpath to $toCls from ${ctx.outersIterator.map(_.owner).toList}")
403-
loop(start, count)
404-
}
405-
catch {
406-
case ex: ClassCastException =>
393+
count: Int = -1): Tree =
394+
try
395+
@tailrec def loop(tree: Tree, count: Int): Tree =
396+
val treeCls = tree.tpe.widen.classSymbol
397+
val outerAccessorCtx = ctx.withPhaseNoLater(ctx.lambdaLiftPhase) // lambdalift mangles local class names, which means we cannot reliably find outer acessors anymore
398+
ctx.log(i"outer to $toCls of $tree: ${tree.tpe}, looking for ${outerAccName(treeCls.asClass)(outerAccessorCtx)} in $treeCls")
399+
if (count == 0 || count < 0 && treeCls == toCls) tree
400+
else
401+
val enclClass = ctx.owner.lexicallyEnclosingClass.asClass
402+
val outerAcc = tree match
403+
case tree: This if tree.symbol == enclClass && !enclClass.is(Trait) =>
404+
outerParamAccessor(enclClass)(using outerAccessorCtx)
405+
case _ =>
406+
outerAccessor(treeCls.asClass)(using outerAccessorCtx)
407+
assert(outerAcc.exists,
408+
i"failure to construct path from ${ctx.owner.ownersIterator.toList}%/% to `this` of ${toCls.showLocated};\n${treeCls.showLocated} does not have an outer accessor")
409+
loop(tree.select(outerAcc).ensureApplied, count - 1)
410+
411+
ctx.log(i"computing outerpath to $toCls from ${ctx.outersIterator.map(_.owner).toList}")
412+
loop(start, count)
413+
catch case ex: ClassCastException =>
407414
throw new ClassCastException(i"no path exists from ${ctx.owner.enclosingClass} to $toCls")
408-
}
409415

410416
/** The outer parameter definition of a constructor if it needs one */
411417
def paramDefs(constr: Symbol): List[ValDef] =

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ object LambdaLift {
2323
import ast.tpd._
2424
private class NoPath extends Exception
2525

26+
val name: String = "lambdaLift"
27+
2628
/** The core lambda lift functionality. */
2729
class Lifter(thisPhase: MiniPhase with DenotTransformer)(implicit ctx: Context) {
2830

@@ -500,7 +502,7 @@ class LambdaLift extends MiniPhase with IdentityDenotTransformer { thisPhase =>
500502
import ast.tpd._
501503

502504
/** the following two members override abstract members in Transform */
503-
val phaseName: String = "lambdaLift"
505+
val phaseName: String = LambdaLift.name
504506

505507
override def relaxedTypingInGroup: Boolean = true
506508
// Because it adds free vars as additional proxy parameters

tests/run/i8033.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
trait Okay extends Serializable {
2+
def okay: Okay
3+
}
4+
5+
class Foo {
6+
def okay1: Okay = new Okay() {
7+
val okay: Okay = this
8+
override def toString = "okay1"
9+
}
10+
def okay2: Okay = new Okay {
11+
val okay: Okay = okay1
12+
override def toString = "okay2"
13+
}
14+
}
15+
16+
object Test {
17+
def main(args: Array[String]): Unit = {
18+
val foo = new Foo
19+
assert(roundTrip(foo.okay1).toString == "okay1")
20+
assert(roundTrip(foo.okay2).toString == "okay2")
21+
}
22+
23+
def roundTrip[A](a: A): A = {
24+
import java.io._
25+
26+
val aos = new ByteArrayOutputStream()
27+
val oos = new ObjectOutputStream(aos)
28+
oos.writeObject(a)
29+
oos.close()
30+
val ais = new ByteArrayInputStream(aos.toByteArray())
31+
val ois: ObjectInputStream = new ObjectInputStream(ais)
32+
val newA = ois.readObject()
33+
ois.close()
34+
newA.asInstanceOf[A]
35+
}
36+
}

tests/run/outer-accessors.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class A:
2+
val a = 2
3+
4+
class B:
5+
val b = 3
6+
7+
trait T:
8+
def t = a + b
9+
10+
val bb = B()
11+
12+
class C extends bb.T:
13+
def result = a + t
14+
15+
@main def Test =
16+
val a = A()
17+
val c = a.C()
18+
assert(c.result == 7)

0 commit comments

Comments
 (0)