Skip to content

Commit e1c5589

Browse files
committed
Make more anonymous functions static
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
1 parent 55c2002 commit e1c5589

File tree

2 files changed

+34
-5
lines changed

2 files changed

+34
-5
lines changed

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

Lines changed: 12 additions & 5 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.*
@@ -184,23 +185,29 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co
184185
def setLogicOwner(local: Symbol) =
185186
val encClass = local.owner.enclosingClass
186187
val preferEncClass =
187-
(
188188
encClass.isStatic
189189
// non-static classes can capture owners, so should be avoided
190190
&& (encClass.isProperlyContainedIn(local.topLevelClass)
191191
// can be false for symbols which are defined in some weird combination of supercalls.
192192
|| encClass.is(ModuleClass, butNot = Package)
193193
// needed to not cause deadlocks in classloader. see t5375.scala
194194
)
195-
)
196-
|| (
195+
&& (!sym.isAnonymousFunction || sym.owner.ownersIterator.exists(_.isConstructor))
196+
// For anonymous functions in static objects, we prefer them to be static because
197+
// that means lambdas are memoized and can be serialized even if the enclosing object
198+
// is not serializable. See run/lambda-serialization-gc.scala and run/i19224.scala.
199+
// On the other hand, we don't want to lift anonymous functions from inside the
200+
// object constructor to be static since that can cause deadlocks by its interaction
201+
// with class initialization. See run/deadlock.scala, which works in Scala 3
202+
// but deadlocks in Scala 2.
203+
||
197204
/* Scala.js: Never move any member beyond the boundary of a DynamicImportThunk.
198205
* DynamicImportThunk subclasses are boundaries between the eventual ES modules
199206
* that can be dynamically loaded. Moving members across that boundary changes
200207
* the dynamic and static dependencies between ES modules, which is forbidden.
201208
*/
202209
ctx.settings.scalajs.value && encClass.isSubClass(jsdefn.DynamicImportThunkClass)
203-
)
210+
204211
logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass
205212

206213
tree match

tests/run/i19224.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
}

0 commit comments

Comments
 (0)