Skip to content

Commit e077c24

Browse files
committed
SI-9390 Emit local defs that don't capture this as static
This avoids unnecessary memory retention, and allows lambdas that call the local methods to be serializable, regardless of whether or not the enclosing class is serializable. The second point is especially pressing, given that the enclosing class for local methods defined in a used to be the (serializable) anonymous function class, but as of Scala 2.12 will be the enclosing class of the lambda. This change is similar in spirit to SI-9408 / 93bee55.
1 parent f01d061 commit e077c24

File tree

8 files changed

+135
-18
lines changed

8 files changed

+135
-18
lines changed

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
225225
}
226226
}
227227

228+
228229
private def transformFunction(originalFunction: Function): Tree = {
229230
val target = targetMethod(originalFunction)
230231
assert(target.hasFlag(Flags.STATIC))
@@ -272,6 +273,12 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
272273
val Template(parents, self, body) = super.transform(deriveTemplate(tree)(_.mapConserve(pretransform)))
273274
Template(parents, self, body ++ boxingBridgeMethods)
274275
} finally boxingBridgeMethods.clear()
276+
case dd: DefDef if dd.symbol.isLiftedMethod && !dd.symbol.isDelambdafyTarget =>
277+
// SI-9390 emit lifted methods that don't require a `this` reference as STATIC
278+
// delambdafy targets are excluded as they are made static by `transformFunction`.
279+
if (!dd.symbol.hasFlag(STATIC) && !methodReferencesThis(dd.symbol))
280+
dd.symbol.setFlag(STATIC)
281+
super.transform(tree)
275282
case _ => super.transform(tree)
276283
}
277284
} // DelambdafyTransformer
@@ -326,19 +333,28 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
326333

327334
// recursively find methods that refer to 'this' directly or indirectly via references to other methods
328335
// for each method found add it to the referrers set
329-
private def refersToThis(symbol: Symbol): Boolean =
330-
(thisReferringMethods contains symbol) ||
331-
(liftedMethodReferences(symbol) exists refersToThis) && {
332-
// add it early to memoize
333-
debuglog(s"$symbol indirectly refers to 'this'")
334-
thisReferringMethods += symbol
335-
true
336+
private def refersToThis(symbol: Symbol): Boolean = {
337+
var seen = mutable.Set[Symbol]()
338+
def loop(symbol: Symbol): Boolean = {
339+
if (seen(symbol)) false
340+
else {
341+
seen += symbol
342+
(thisReferringMethods contains symbol) ||
343+
(liftedMethodReferences(symbol) exists loop) && {
344+
// add it early to memoize
345+
debuglog(s"$symbol indirectly refers to 'this'")
346+
thisReferringMethods += symbol
347+
true
348+
}
336349
}
350+
}
351+
loop(symbol)
352+
}
337353

338354
private var currentMethod: Symbol = NoSymbol
339355

340356
override def traverse(tree: Tree) = tree match {
341-
case DefDef(_, _, _, _, _, _) if tree.symbol.isDelambdafyTarget =>
357+
case DefDef(_, _, _, _, _, _) if tree.symbol.isDelambdafyTarget || tree.symbol.isLiftedMethod =>
342358
// we don't expect defs within defs. At this phase trees should be very flat
343359
if (currentMethod.exists) devWarning("Found a def within a def at a phase where defs are expected to be flattened out.")
344360
currentMethod = tree.symbol
@@ -349,6 +365,9 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
349365
// They'll be of the form {(args...) => this.anonfun(args...)}
350366
// but we do need to make note of the lifted body method in case it refers to 'this'
351367
if (currentMethod.exists) liftedMethodReferences(currentMethod) += targetMethod(fun)
368+
case Apply(sel @ Select(This(_), _), args) if sel.symbol.isLiftedMethod =>
369+
if (currentMethod.exists) liftedMethodReferences(currentMethod) += sel.symbol
370+
super.traverseTrees(args)
352371
case This(_) =>
353372
if (currentMethod.exists && tree.symbol == currentMethod.enclClass) {
354373
debuglog(s"$currentMethod directly refers to 'this'")

test/files/run/t5652.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
public default int T1.T1$$g$1()
21
public default int T1.f0()
32
public default void T1.$init$()
4-
public final int A1.A1$$g$2()
3+
public static int T1.T1$$g$1()
54
public int A1.f1()
6-
public final int A2.A2$$g$1()
5+
public static final int A1.A1$$g$2()
76
public int A2.f2()
7+
public static final int A2.A2$$g$1()

test/files/run/t5652b.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
private final int A1.g$1()
1+
private static final int A1.g$1()
22
public int A1.f1()
3-
private final int A2.g$1()
3+
private static final int A2.g$1()
44
public int A2.f2()

test/files/run/t5652c.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
public final int A1.A1$$g$1()
2-
public final int A1.A1$$g$2()
31
public int A1.f1()
42
public int A1.f2()
3+
public static final int A1.A1$$g$1()
4+
public static final int A1.A1$$g$2()
55
1
66
2

test/files/run/t9390.scala

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
class C {
2+
def methodLift1 = {
3+
def isEven(c: Int) = c % 2 == 0
4+
val f: Int => Boolean = isEven
5+
f
6+
}
7+
def methodLift2 = {
8+
def isEven(c: Int) = c % 2 == 0
9+
def isEven0(c: Int) = isEven(c)
10+
val f: Int => Boolean = isEven0
11+
f
12+
}
13+
14+
def methodLift3 = {
15+
def isEven(c: Int) = {toString; c % 2 == 0}
16+
def isEven0(c: Int) = isEven(c)
17+
val f: Int => Boolean = isEven0
18+
f
19+
}
20+
}
21+
22+
object Test {
23+
def main(args: Array[String]): Unit = {
24+
val c = new C
25+
26+
{
27+
val f = c.methodLift1
28+
assert(f(0))
29+
assert(!f(1))
30+
val f1 = serializeDeserialize(f)
31+
assert(f1(0))
32+
assert(!f1(1))
33+
}
34+
35+
36+
{
37+
val f = c.methodLift2
38+
assert(f(0))
39+
assert(!f(1))
40+
val f1 = serializeDeserialize(f)
41+
assert(f1(0))
42+
assert(!f1(1))
43+
}
44+
45+
{
46+
val f = c.methodLift3
47+
assert(f(0))
48+
assert(!f(1))
49+
try {
50+
serializeDeserialize(this)
51+
assert(false)
52+
} catch {
53+
case _: java.io.NotSerializableException =>
54+
// expected, the closure in methodLift3 must capture C which is not serializable
55+
}
56+
}
57+
}
58+
59+
def serializeDeserialize[T <: AnyRef](obj: T): T = {
60+
import java.io._
61+
val buffer = new ByteArrayOutputStream
62+
val out = new ObjectOutputStream(buffer)
63+
out.writeObject(obj)
64+
val in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray))
65+
in.readObject.asInstanceOf[T]
66+
}
67+
}

test/files/run/t9390b.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class C { // C is not serializable
2+
def foo = (x: Int) => (y: Int) => x + y
3+
def bar = (x: Int) => (y: Int) => {toString; x + y}
4+
}
5+
6+
object Test {
7+
def main(args: Array[String]): Unit = {
8+
val c = new C
9+
val f = c.foo
10+
assert(f(1)(2) == 3)
11+
val f1 = serializeDeserialize(f)
12+
assert(f1(1)(2) == 3)
13+
14+
try {
15+
serializeDeserialize(c.bar)
16+
assert(false)
17+
} catch {
18+
case _: java.io.NotSerializableException =>
19+
// expected, lambda transitively refers to this
20+
}
21+
}
22+
23+
def serializeDeserialize[T <: AnyRef](obj: T): T = {
24+
import java.io._
25+
val buffer = new ByteArrayOutputStream
26+
val out = new ObjectOutputStream(buffer)
27+
out.writeObject(obj)
28+
val in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray))
29+
in.readObject.asInstanceOf[T]
30+
}
31+
}

test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class InlineWarningTest extends BytecodeTesting {
113113

114114
val warn =
115115
"""M::f()I is annotated @inline but could not be inlined:
116-
|The callee M::f()I contains the instruction INVOKESPECIAL M.nested$1 ()I
116+
|The callee M::f()I contains the instruction INVOKESTATIC M.nested$1 ()I
117117
|that would cause an IllegalAccessError when inlined into class N""".stripMargin
118118

119119
var c = 0
@@ -140,7 +140,7 @@ class InlineWarningTest extends BytecodeTesting {
140140

141141
val warn =
142142
"""M::f(Lscala/Function1;)I could not be inlined:
143-
|The callee M::f(Lscala/Function1;)I contains the instruction INVOKESPECIAL M.nested$1 ()I
143+
|The callee M::f(Lscala/Function1;)I contains the instruction INVOKESTATIC M.nested$1 ()I
144144
|that would cause an IllegalAccessError when inlined into class N""".stripMargin
145145

146146
var c = 0

test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1141,7 +1141,7 @@ class InlinerTest extends BytecodeTesting {
11411141

11421142
val warn =
11431143
"""C::h()I is annotated @inline but could not be inlined:
1144-
|The callee C::h()I contains the instruction INVOKESPECIAL C.f$1 ()I
1144+
|The callee C::h()I contains the instruction INVOKESTATIC C.f$1 ()I
11451145
|that would cause an IllegalAccessError when inlined into class D.""".stripMargin
11461146

11471147
val List(c, d) = compile(code, allowMessage = _.msg contains warn)

0 commit comments

Comments
 (0)