Skip to content

Commit f07019f

Browse files
committed
SI-9390 Avoid needless outer capture with local classes
An existing optimization in `Constructors` elides the outer field in member and local classes, if the class doesn't use the outer reference. (Member classes also need to be final, which is a secret handshake to say we're also happy to weaken prefix matching in the pattern matcher.) That optimization leaves the constructor signature as is: the constructor still accepts the outer instance, but does not store it. For member classes, this means that we can separately compile code that calls the constructor. Local classes need not be hampered by this constraint, we could remove the outer instance from the constructor call too. Why would we want to do this? Let's look at the case before and after this commit. Before: ``` class C extends Object { def foo(): Function1 = $anonfun(); final <static> <artifact> def $anonfun$foo$1($this: C, x: Object): Object = new <$anon: Object>($this); def <init>(): C = { C.super.<init>(); () } }; final class anon$1 extends Object { def <init>($outer: C): <$anon: Object> = { anon$1.super.<init>(); () } } ``` After: ``` class C extends Object { def foo(): Function1 = $anonfun(); final <static> <artifact> def $anonfun$foo$1(x: Object): Object = new <$anon: Object>(null); def <init>(): C = { C.super.<init>(); () } }; final class anon$1 extends Object { def <init>($outer: C): <$anon: Object> = { anon$1.super.<init>(); () } } ``` However, the status quo means that a lambda that This in turn makes lambdas that refer to such classes serializable even when the outer class is not itself serialiable. I have not attempted to extend this to calls to secondary constructors.
1 parent e077c24 commit f07019f

File tree

10 files changed

+57
-15
lines changed

10 files changed

+57
-15
lines changed

src/compiler/scala/tools/nsc/transform/Constructors.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,9 @@ abstract class Constructors extends Statics with Transform with ast.TreeDSL {
715715
primaryConstrBody.expr)
716716
})
717717

718+
if (omittableAccessor.exists(_.isOuterField) && !constructorStats.exists(_.exists { case i: Ident if i.symbol.isOuterParam => true; case _ => false}))
719+
primaryConstructor.symbol.updateAttachment(OuterArgCanBeElided)
720+
718721
val constructors = primaryConstructor :: auxConstructors
719722

720723
// Unlink all fields that can be dropped from class scope

src/compiler/scala/tools/nsc/transform/Delambdafy.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,15 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
279279
if (!dd.symbol.hasFlag(STATIC) && !methodReferencesThis(dd.symbol))
280280
dd.symbol.setFlag(STATIC)
281281
super.transform(tree)
282+
case Apply(fun, outer :: rest) if shouldElideOuterArg(fun.symbol, outer) =>
283+
val nullOuter = gen.mkZero(outer.tpe)
284+
treeCopy.Apply(tree, transform(fun), nullOuter :: transformTrees(rest))
282285
case _ => super.transform(tree)
283286
}
284287
} // DelambdafyTransformer
285288

289+
private def shouldElideOuterArg(fun: Symbol, outerArg: Tree): Boolean =
290+
fun.isConstructor && treeInfo.isQualifierSafeToElide(outerArg) && fun.hasAttachment[OuterArgCanBeElided.type]
286291

287292
// A traverser that finds symbols used but not defined in the given Tree
288293
// TODO freeVarTraverser in LambdaLift does a very similar task. With some
@@ -368,6 +373,9 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
368373
case Apply(sel @ Select(This(_), _), args) if sel.symbol.isLiftedMethod =>
369374
if (currentMethod.exists) liftedMethodReferences(currentMethod) += sel.symbol
370375
super.traverseTrees(args)
376+
case Apply(fun, outer :: rest) if shouldElideOuterArg(fun.symbol, outer) =>
377+
super.traverse(fun)
378+
super.traverseTrees(rest)
371379
case This(_) =>
372380
if (currentMethod.exists && tree.symbol == currentMethod.enclClass) {
373381
debuglog(s"$currentMethod directly refers to 'this'")

src/compiler/scala/tools/nsc/transform/ExplicitOuter.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ abstract class ExplicitOuter extends InfoTransform
6767
result
6868
}
6969

70-
private val innerClassConstructorParamName: TermName = newTermName("arg" + nme.OUTER)
71-
7270
class RemoveBindingsTransformer(toRemove: Set[Symbol]) extends Transformer {
7371
override def transform(tree: Tree) = tree match {
7472
case Bind(_, body) if toRemove(tree.symbol) => super.transform(body)
@@ -169,7 +167,7 @@ abstract class ExplicitOuter extends InfoTransform
169167

170168
val paramsWithOuter =
171169
if (sym.isClassConstructor && isInner(sym.owner)) // 1
172-
sym.newValueParameter(innerClassConstructorParamName, sym.pos).setInfo(sym.owner.outerClass.thisType) :: params
170+
sym.newValueParameter(nme.OUTER_ARG, sym.pos).setInfo(sym.owner.outerClass.thisType) :: params
173171
else params
174172

175173
if ((resTpTransformed ne resTp) || (paramsWithOuter ne params)) MethodType(paramsWithOuter, resTpTransformed)

src/reflect/scala/reflect/internal/StdAttachments.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ trait StdAttachments {
7171
abstract class InlineAnnotatedAttachment
7272
case object NoInlineCallsiteAttachment extends InlineAnnotatedAttachment
7373
case object InlineCallsiteAttachment extends InlineAnnotatedAttachment
74+
75+
/** Attached to a local class that has its outer field elided. A `null` constant may be passed
76+
* in place of the outer parameter, can help callers to avoid capturing the outer instance.
77+
*/
78+
case object OuterArgCanBeElided extends PlainAttachment
7479
}

src/reflect/scala/reflect/internal/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ trait StdNames {
364364
val MODULE_INSTANCE_FIELD: NameType = NameTransformer.MODULE_INSTANCE_NAME // "MODULE$"
365365
val OUTER: NameType = "$outer"
366366
val OUTER_LOCAL: NameType = OUTER.localName
367+
val OUTER_ARG: NameType = "arg" + OUTER
367368
val OUTER_SYNTH: NameType = "<outer>" // emitted by virtual pattern matcher, replaced by outer accessor in explicitouter
368369
val ROOTPKG: NameType = "_root_"
369370
val SELECTOR_DUMMY: NameType = "<unapply-selector>"

src/reflect/scala/reflect/internal/Symbols.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,9 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
914914
/** Is this symbol an accessor method for outer? */
915915
final def isOuterField = isArtifact && (unexpandedName == nme.OUTER_LOCAL)
916916

917+
/** Is this symbol an outer parameter in a constructor */
918+
final def isOuterParam = isParameter && owner.isConstructor && (name == nme.OUTER_ARG || name == nme.OUTER)
919+
917920
/** Does this symbol denote a stable value, ignoring volatility?
918921
*
919922
* Stability and volatility are checked separately to allow volatile paths in patterns that amount to equality checks. SI-6815

src/reflect/scala/reflect/runtime/JavaUniverseForce.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse =>
4545
this.SubpatternsAttachment
4646
this.NoInlineCallsiteAttachment
4747
this.InlineCallsiteAttachment
48+
this.OuterArgCanBeElided
4849
this.noPrint
4950
this.typeDebug
5051
this.Range

test/files/jvm/t9105.check

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
#partest -Ydelambdafy:inline
2-
(class C$$anonfun$1$A$1,class C$$anonfun$1,null)
3-
(class C$$anonfun$1$B$1,class C$$anonfun$1,private final java.lang.Object C$$anonfun$1.m$1())
4-
(class C$$anonfun$1$C$1,class C$$anonfun$1,null)
5-
(class C$$anonfun$1$$anonfun$2$D$1,class C$$anonfun$1$$anonfun$2,null)
6-
(class C$$anonfun$met$1$E$1,class C$$anonfun$met$1,null)
7-
(class C$$anonfun$met$1$F$1,class C$$anonfun$met$1,private final java.lang.Object C$$anonfun$met$1.m$2())
8-
(class C$$anonfun$met$1$G$1,class C$$anonfun$met$1,null)
9-
(class C$$anonfun$met$1$$anonfun$3$H$1,class C$$anonfun$met$1$$anonfun$3,null)
10-
#partest !-Ydelambdafy:inline
111
(class C$A$1,class C,null)
12-
(class C$B$1,class C,private final java.lang.Object C.m$1())
2+
(class C$B$1,class C,private static final java.lang.Object C.m$1())
133
(class C$C$1,class C,null)
144
(class C$D$1,class C,null)
155
(class C$E$1,class C,public scala.Function0 C.met())
16-
(class C$F$1,class C,private final java.lang.Object C.m$2())
6+
(class C$F$1,class C,private static final java.lang.Object C.m$2())
177
(class C$G$1,class C,public scala.Function0 C.met())
188
(class C$H$1,class C,public scala.Function0 C.met())

test/files/run/t9390c.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class C { // C is not serializable
2+
def foo = {
3+
{ (x: Any) => new Object {} }
4+
}
5+
}
6+
object Test {
7+
def main(args: Array[String]): Unit = {
8+
val c = new C
9+
val f = c.foo
10+
val f1 = serializeDeserialize(f)
11+
}
12+
13+
def serializeDeserialize[T <: AnyRef](obj: T): T = {
14+
import java.io._
15+
val buffer = new ByteArrayOutputStream
16+
val out = new ObjectOutputStream(buffer)
17+
out.writeObject(obj)
18+
val in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray))
19+
in.readObject.asInstanceOf[T]
20+
}
21+
}

test/files/run/t9390d.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class C { // C is not serializable
2+
def foo: () => Any = {
3+
{ () => class UseOuterInConstructor { C.this.toString }; new UseOuterInConstructor : Any}
4+
}
5+
}
6+
object Test {
7+
def main(args: Array[String]): Unit = {
8+
val c = new C
9+
val f = c.foo
10+
f() // Doesn't NPE, as we didn't elide the outer instance in the constructor call.
11+
}
12+
}

0 commit comments

Comments
 (0)