Skip to content

Commit 9f110e2

Browse files
authored
Merge pull request #4289 from dotty-staging/fix-1692
Fix #1692: Null out fields after use in lazy initialization
2 parents be25213 + d800e31 commit 9f110e2

File tree

5 files changed

+557
-249
lines changed

5 files changed

+557
-249
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class Compiler {
8787
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods
8888
new Getters, // Replace non-private vals and vars with getter defs (fields are added later)
8989
new ElimByName, // Expand by-name parameter references
90+
new CollectNullableFields, // Collect fields that can be nulled out after use in lazy initialization
9091
new ElimOuterSelect, // Expand outer selections
9192
new AugmentScala2Traits, // Expand traits defined in Scala 2.x to simulate old-style rewritings
9293
new ResolveSuper, // Implement super accessors and add forwarders to trait methods
@@ -97,7 +98,7 @@ class Compiler {
9798
List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements.
9899
List(new ElimErasedValueType, // Expand erased value types to their underlying implmementation types
99100
new VCElideAllocations, // Peep-hole optimization to eliminate unnecessary value class allocations
100-
new Mixin, // Expand trait fields and trait initializers
101+
new Mixin, // Expand trait fields and trait initializers
101102
new LazyVals, // Expand lazy vals
102103
new Memoize, // Add private fields to getters and setters
103104
new NonLocalReturns, // Expand non-local returns

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ object Phases {
211211
private[this] var myTyperPhase: Phase = _
212212
private[this] var mySbtExtractDependenciesPhase: Phase = _
213213
private[this] var myPicklerPhase: Phase = _
214+
private[this] var myCollectNullableFieldsPhase: Phase = _
214215
private[this] var myRefChecksPhase: Phase = _
215216
private[this] var myPatmatPhase: Phase = _
216217
private[this] var myElimRepeatedPhase: Phase = _
@@ -226,6 +227,7 @@ object Phases {
226227
final def typerPhase = myTyperPhase
227228
final def sbtExtractDependenciesPhase = mySbtExtractDependenciesPhase
228229
final def picklerPhase = myPicklerPhase
230+
final def collectNullableFieldsPhase = myCollectNullableFieldsPhase
229231
final def refchecksPhase = myRefChecksPhase
230232
final def patmatPhase = myPatmatPhase
231233
final def elimRepeatedPhase = myElimRepeatedPhase
@@ -244,6 +246,7 @@ object Phases {
244246
myTyperPhase = phaseOfClass(classOf[FrontEnd])
245247
mySbtExtractDependenciesPhase = phaseOfClass(classOf[sbt.ExtractDependencies])
246248
myPicklerPhase = phaseOfClass(classOf[Pickler])
249+
myCollectNullableFieldsPhase = phaseOfClass(classOf[CollectNullableFields])
247250
myRefChecksPhase = phaseOfClass(classOf[RefChecks])
248251
myElimRepeatedPhase = phaseOfClass(classOf[ElimRepeated])
249252
myExtensionMethodsPhase = phaseOfClass(classOf[ExtensionMethods])
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package dotty.tools.dotc.transform
2+
3+
import dotty.tools.dotc.ast.tpd
4+
import dotty.tools.dotc.core.Contexts.Context
5+
import dotty.tools.dotc.core.Flags._
6+
import dotty.tools.dotc.core.Symbols.Symbol
7+
import dotty.tools.dotc.core.Types.{Type, ExprType}
8+
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
9+
import dotty.tools.dotc.transform.SymUtils._
10+
11+
import scala.collection.JavaConverters._
12+
import scala.collection.mutable
13+
14+
import java.util.IdentityHashMap
15+
16+
object CollectNullableFields {
17+
val name = "collectNullableFields"
18+
}
19+
20+
/** Collect fields that can be nulled out after use in lazy initialization.
21+
*
22+
* This information is used during lazy val transformation to assign null to private
23+
* fields that are only used within a lazy val initializer. This is not just an optimization,
24+
* but is needed for correctness to prevent memory leaks. E.g.
25+
*
26+
* ```scala
27+
* class TestByNameLazy(byNameMsg: => String) {
28+
* lazy val byLazyValMsg = byNameMsg
29+
* }
30+
* ```
31+
*
32+
* Here `byNameMsg` should be null out once `byLazyValMsg` is
33+
* initialised.
34+
*
35+
* A field is nullable if all the conditions below hold:
36+
* - belongs to a non trait-class
37+
* - is private[this]
38+
* - is not lazy
39+
* - its type is nullable
40+
* - is only used in a lazy val initializer
41+
* - defined in the same class as the lazy val
42+
*/
43+
class CollectNullableFields extends MiniPhase {
44+
import tpd._
45+
46+
override def phaseName = CollectNullableFields.name
47+
48+
/** Running after `ElimByName` to see by names as nullable types. */
49+
override def runsAfter = Set(ElimByName.name)
50+
51+
private[this] sealed trait FieldInfo
52+
private[this] case object NotNullable extends FieldInfo
53+
private[this] case class Nullable(by: Symbol) extends FieldInfo
54+
55+
/** Whether or not a field is nullable */
56+
private[this] var nullability: IdentityHashMap[Symbol, FieldInfo] = _
57+
58+
override def prepareForUnit(tree: Tree)(implicit ctx: Context) = {
59+
if (nullability == null) nullability = new IdentityHashMap
60+
ctx
61+
}
62+
63+
private def recordUse(tree: Tree)(implicit ctx: Context): Tree = {
64+
val sym = tree.symbol
65+
val isNullablePrivateField =
66+
sym.isField &&
67+
!sym.is(Lazy) &&
68+
!sym.owner.is(Trait) &&
69+
sym.initial.is(PrivateLocal) &&
70+
sym.info.widenDealias.typeSymbol.isNullableClass
71+
72+
if (isNullablePrivateField)
73+
nullability.get(sym) match {
74+
case Nullable(from) if from != ctx.owner => // used in multiple lazy val initializers
75+
nullability.put(sym, NotNullable)
76+
case null => // not in the map
77+
val from = ctx.owner
78+
val isNullable =
79+
from.is(Lazy, butNot = Module) && // is lazy val
80+
from.owner.isClass && // is field
81+
from.owner.eq(sym.owner) // is lazy val and field defined in the same class
82+
val info = if (isNullable) Nullable(from) else NotNullable
83+
nullability.put(sym, info)
84+
case _ =>
85+
// Do nothing for:
86+
// - NotNullable
87+
// - Nullable(ctx.owner)
88+
}
89+
90+
tree
91+
}
92+
93+
override def transformIdent(tree: Ident)(implicit ctx: Context) =
94+
recordUse(tree)
95+
96+
override def transformSelect(tree: Select)(implicit ctx: Context) =
97+
recordUse(tree)
98+
99+
/** Map lazy values to the fields they should null after initialization. */
100+
def lazyValNullables(implicit ctx: Context): IdentityHashMap[Symbol, mutable.ListBuffer[Symbol]] = {
101+
val result = new IdentityHashMap[Symbol, mutable.ListBuffer[Symbol]]
102+
103+
nullability.forEach {
104+
case (sym, Nullable(from)) =>
105+
val bldr = result.computeIfAbsent(from, _ => new mutable.ListBuffer)
106+
bldr += sym
107+
case _ =>
108+
}
109+
110+
result
111+
}
112+
}

0 commit comments

Comments
 (0)