Skip to content

Commit f8f2ae2

Browse files
authored
Make more anonymous functions static (#19251)
An anonymous function in a static object was previously mapped to a member of that object. We now map it to a static member of the toplevel class instead. This causes the backend to memoize the function, which fixes #19224. On the other hand, we don't do that for anonymous functions nested in the object constructor, since that can cause deadlocks (see run/deadlock.scala). Scala 2's behavior is different: it does lift lambdas in constructors to be static, too, which can cause deadlocks. Fixes #19224
2 parents c717427 + 7878dbc commit f8f2ae2

File tree

2 files changed

+55
-8
lines changed

2 files changed

+55
-8
lines changed

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

Lines changed: 30 additions & 8 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 core.*
@@ -181,26 +182,47 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co
181182
if enclClass.isContainedIn(thisClass) then thisClass
182183
else enclClass) // unknown this reference, play it safe and assume the narrowest possible owner
183184

185+
/** Set the first owner of a local method or class that's nested inside a term.
186+
* This is either the enclosing package or the enclosing class. If the former,
187+
* the method will be be translated to a static method of its toplevel class.
188+
* In that case, we might later re-adjust the owner to a nested class via
189+
* `narrowTo` when we see that the method refers to the this-type of that class.
190+
* We choose the enclosing package when there's something potentially to gain from this
191+
* and when it is safe to do so
192+
*/
184193
def setLogicOwner(local: Symbol) =
185194
val encClass = local.owner.enclosingClass
195+
// When to prefer the enclosing class over the enclosing package:
186196
val preferEncClass =
187-
(
188197
encClass.isStatic
189-
// non-static classes can capture owners, so should be avoided
198+
// If class is not static, we try to hoist the method out of
199+
// the class to avoid the outer pointer.
190200
&& (encClass.isProperlyContainedIn(local.topLevelClass)
191-
// can be false for symbols which are defined in some weird combination of supercalls.
201+
// If class is nested in an outer object, we prefer to leave the method in the class,
202+
// since putting it in the outer object makes access more complicated
192203
|| encClass.is(ModuleClass, butNot = Package)
193-
// needed to not cause deadlocks in classloader. see t5375.scala
204+
// If class is an outermost object we also want to avoid making the
205+
// method static since that could cause deadlocks in interacting
206+
// with class initialization. See deadlock.scala
194207
)
195-
)
196-
|| (
208+
&& (!sym.isAnonymousFunction || sym.owner.ownersIterator.exists(_.isConstructor))
209+
// The previous conditions mean methods in static objects and nested static classes
210+
// don't get lifted out to be static. In general it is prudent to do that. However,
211+
// for anonymous functions, we prefer them to be static because that means lambdas
212+
// are memoized and can be serialized even if the enclosing object or class
213+
// is not serializable. See run/lambda-serialization-gc.scala and run/i19224.scala.
214+
// On the other hand, we don't want to lift anonymous functions from inside the
215+
// object or class constructor to be static since that can cause again deadlocks
216+
// by its interaction with class initialization. See run/deadlock.scala, which works
217+
// in Scala 3 but deadlocks in Scala 2.
218+
||
197219
/* Scala.js: Never move any member beyond the boundary of a DynamicImportThunk.
198220
* DynamicImportThunk subclasses are boundaries between the eventual ES modules
199221
* that can be dynamically loaded. Moving members across that boundary changes
200222
* the dynamic and static dependencies between ES modules, which is forbidden.
201223
*/
202224
ctx.settings.scalajs.value && encClass.isSubClass(jsdefn.DynamicImportThunkClass)
203-
)
225+
204226
logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass
205227

206228
tree match

tests/run/i19224.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// scalajs: --skip
2+
3+
object Test extends App {
4+
val field = 1
5+
def x(): Int => String = (i: Int) => i.toString
6+
def y(): () => String = () => field.toString
7+
8+
locally {
9+
assert(x() == x()) // true on Scala 2, was false on Scala 3...
10+
assert(y() == y()) // also true if `y` accesses object-local fields
11+
12+
def z(): Int => String = (i: Int) => i.toString
13+
assert(z() != z()) // lambdas in constructor are not lifted to static, so no memoization (Scala 2 lifts them, though).
14+
}
15+
16+
val t1 = new C
17+
val t2 = new C
18+
19+
locally {
20+
assert(t1.x() == t2.x()) // true on Scala 2, was false on Scala 3...
21+
}
22+
}
23+
class C {
24+
def x(): Int => String = (i: Int) => i.toString
25+
}

0 commit comments

Comments
 (0)