Skip to content

Commit 526c307

Browse files
committed
Lambda-owned param ref in ctor incurs no field
1 parent 0be2091 commit 526c307

File tree

5 files changed

+112
-41
lines changed

5 files changed

+112
-41
lines changed

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

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
4545
// to more symbols being retained as parameters. Test case in run/capturing.scala.
4646

4747
/** The private vals that are known to be retained as class fields */
48-
private val retainedPrivateVals = mutable.Set[Symbol]()
48+
private val retainedPrivateVals = mutable.Set.empty[Symbol]
4949

5050
/** The private vals whose definition comes before the current focus */
51-
private val seenPrivateVals = mutable.Set[Symbol]()
51+
private val seenPrivateVals = mutable.Set.empty[Symbol]
5252

5353
// Collect all private parameter accessors and value definitions that need
5454
// to be retained. There are several reasons why a parameter accessor or
@@ -57,31 +57,37 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
5757
// 2. It is accessed before it is defined
5858
// 3. It is accessed on an object other than `this`
5959
// 4. It is a mutable parameter accessor
60-
// 5. It is has a wildcard initializer `_`
61-
private def markUsedPrivateSymbols(tree: RefTree)(using Context): Unit = {
60+
// 5. It has a wildcard initializer `_`
61+
private def markUsedPrivateSymbols(tree: RefTree)(using Context): Unit =
6262

6363
val sym = tree.symbol
6464
def retain() = retainedPrivateVals.add(sym)
6565

66-
if (sym.exists && sym.owner.isClass && mightBeDropped(sym)) {
67-
val owner = sym.owner.asClass
68-
69-
tree match {
70-
case Ident(_) | Select(This(_), _) =>
71-
def inConstructor = {
72-
val method = ctx.owner.enclosingMethod
73-
method.isPrimaryConstructor && ctx.owner.enclosingClass == owner
74-
}
75-
if (inConstructor &&
76-
(sym.is(ParamAccessor) || seenPrivateVals.contains(sym))) {
77-
// used inside constructor, accessed on this,
78-
// could use constructor argument instead, no need to retain field
79-
}
80-
else retain()
81-
case _ => retain()
82-
}
83-
}
84-
}
66+
if sym.exists && sym.owner.isClass && mightBeDropped(sym) then
67+
tree match
68+
case Ident(_) | Select(This(_), _) =>
69+
val method = ctx.owner.enclosingMethod
70+
// template exprs are moved (below) to constructor, where lifted anonfun will take its captured env as an arg
71+
inline def inAnonFunInCtor =
72+
method.isAnonymousFunction
73+
&& (
74+
method.owner.isLocalDummy
75+
||
76+
method.owner.owner == sym.owner && !method.owner.isOneOf(MethodOrLazy)
77+
)
78+
&& !sym.owner.is(Module) // lambdalift doesn't transform correctly (to do)
79+
val inConstructor =
80+
(method.isPrimaryConstructor || inAnonFunInCtor)
81+
&& ctx.owner.enclosingClass == sym.owner
82+
val noField =
83+
inConstructor
84+
&& (sym.is(ParamAccessor) || seenPrivateVals.contains(sym))
85+
// used inside constructor, accessed on this,
86+
// could use constructor argument instead, no need to retain field
87+
if !noField then
88+
retain()
89+
case _ =>
90+
retain()
8591

8692
override def transformIdent(tree: tpd.Ident)(using Context): tpd.Tree = {
8793
markUsedPrivateSymbols(tree)
@@ -184,6 +190,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
184190
transform(tree).changeOwnerAfter(prevOwner, constr.symbol, thisPhase)
185191
}
186192

193+
// mightBeDropped is trivially false for NoSymbol -> NoSymbol isRetained
187194
def isRetained(acc: Symbol) =
188195
!mightBeDropped(acc) || retainedPrivateVals(acc)
189196

@@ -209,30 +216,28 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
209216
}
210217
}
211218

212-
val dropped = mutable.Set[Symbol]()
219+
val dropped = mutable.Set.empty[Symbol]
213220

214221
// Split class body into statements that go into constructor and
215222
// definitions that are kept as members of the class.
216-
def splitStats(stats: List[Tree]): Unit = stats match {
217-
case stat :: stats1 =>
223+
def splitStats(stats: List[Tree]): Unit = stats match
224+
case stat :: stats =>
225+
val sym = stat.symbol
218226
stat match {
219-
case stat @ ValDef(name, tpt, _) if !stat.symbol.is(Lazy) && !stat.symbol.hasAnnotation(defn.ScalaStaticAnnot) =>
220-
val sym = stat.symbol
227+
case stat @ ValDef(name, tpt, _) if !sym.is(Lazy) && !sym.hasAnnotation(defn.ScalaStaticAnnot) =>
221228
if (isRetained(sym)) {
222229
if (!stat.rhs.isEmpty && !isWildcardArg(stat.rhs))
223230
constrStats += Assign(ref(sym), intoConstr(stat.rhs, sym)).withSpan(stat.span)
224231
clsStats += cpy.ValDef(stat)(rhs = EmptyTree)
225232
}
226-
else if (!stat.rhs.isEmpty) {
227-
dropped += sym
228-
sym.copySymDenotation(
229-
initFlags = sym.flags &~ Private,
230-
owner = constr.symbol).installAfter(thisPhase)
231-
constrStats += intoConstr(stat, sym)
232-
} else
233+
else
233234
dropped += sym
235+
if !stat.rhs.isEmpty then
236+
sym.copySymDenotation(
237+
initFlags = sym.flags &~ Private,
238+
owner = constr.symbol).installAfter(thisPhase)
239+
constrStats += intoConstr(stat, sym)
234240
case stat @ DefDef(name, _, tpt, _) if stat.symbol.isGetter && !stat.symbol.is(Lazy) =>
235-
val sym = stat.symbol
236241
assert(isRetained(sym), sym)
237242
if sym.isConstExprFinalVal then
238243
if stat.rhs.isInstanceOf[Literal] then
@@ -271,9 +276,9 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
271276
case _ =>
272277
constrStats += intoConstr(stat, tree.symbol)
273278
}
274-
splitStats(stats1)
279+
splitStats(stats)
275280
case Nil =>
276-
}
281+
end splitStats
277282

278283
/** Check that we do not have both a private field with name `x` and a private field
279284
* with name `FieldName(x)`. These will map to the same JVM name and therefore cause
@@ -303,14 +308,15 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
303308
dropped += acc
304309
Nil
305310
}
306-
else if (!isRetained(acc.field)) { // It may happen for unit fields, tests/run/i6987.scala
311+
else if (acc.field.exists && !isRetained(acc.field)) { // It may happen for unit fields, tests/run/i6987.scala
307312
dropped += acc.field
308313
Nil
309314
}
310315
else {
311316
val param = acc.subst(accessors, paramSyms)
312-
if (param.hasAnnotation(defn.ConstructorOnlyAnnot))
313-
report.error(em"${acc.name} is marked `@constructorOnly` but it is retained as a field in ${acc.owner}", acc.srcPos)
317+
if param.hasAnnotation(defn.ConstructorOnlyAnnot) then
318+
val msg = em"${acc.name} is marked `@constructorOnly` but it is retained as a field in ${acc.owner}"
319+
report.error(msg, acc.srcPos)
314320
val target = if (acc.is(Method)) acc.field else acc
315321
if (!target.exists) Nil // this case arises when the parameter accessor is an alias
316322
else {

tests/neg/i22979.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
import annotation.*
3+
4+
class C(@constructorOnly s: String): // error
5+
def g: Any => String = _ => s
6+
def f[A](xs: List[A]): List[String] = xs.map(g)
7+
8+
import scala.util.boundary
9+
10+
class Leak()(using @constructorOnly l: boundary.Label[String]): // error
11+
lazy val broken = Option("stop").foreach(boundary.break(_))

tests/pos/i22979.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
import annotation.*
3+
4+
class C(@constructorOnly s: String):
5+
val g: Any => String = _ => s
6+
def f[A](xs: List[A]): List[String] = xs.map(g)
7+
8+
import scala.util.boundary
9+
10+
class Leak()(using @constructorOnly l: boundary.Label[String]):
11+
Option("stop").foreach(boundary.break(_))
12+
13+
14+
class Lapse:
15+
def f = Lapse.DefaultSentinelFn()
16+
object Lapse:
17+
private val DefaultSentinel: AnyRef = new AnyRef
18+
private val DefaultSentinelFn: () => AnyRef = () => DefaultSentinel

tests/run/i22979/Leak.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import annotation.*
2+
3+
class Leak()(using @constructorOnly l: boundary.Label[String]) {
4+
Seq("a", "b").foreach(_ => boundary.break("stop"))
5+
}
6+
7+
object boundary {
8+
final class Break[T] private[boundary](val label: Label[T], val value: T)
9+
extends RuntimeException(
10+
/*message*/ null, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false)
11+
final class Label[-T]
12+
def break[T](value: T)(using label: Label[T]): Nothing = throw new Break(label, value)
13+
}

tests/run/i22979/test.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// scalajs: --skip
2+
3+
import util.Try
4+
5+
@main def Test =
6+
assert(Try(classOf[Leak].getDeclaredField("l")).isFailure)
7+
assert(classOf[Leak].getFields.length == 0)
8+
//classOf[Leak].getFields.map(_.getName).foreach(println) //DEBUG
9+
assert(classOf[C].getFields.length == 0)
10+
//classOf[Lapse.type].getFields.map(_.getName).foreach(println) //DEBUG
11+
12+
class C:
13+
private val x = 42
14+
println(x)
15+
println(List(27).map(_ + x))
16+
17+
// The easy tweak for lambdas does not work for module owner.
18+
// The lambdalifted anonfun is not transformed correctly.
19+
class Lapse:
20+
def f = Lapse.DefaultSentinelFn()
21+
object Lapse:
22+
private val DefaultSentinel: AnyRef = new AnyRef
23+
private val DefaultSentinelFn: () => AnyRef = () => DefaultSentinel

0 commit comments

Comments
 (0)