Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit f1e3b3e

Browse files
committed
Lambda-owned param ref in ctor incurs no field
1 parent ef37ebf commit f1e3b3e

File tree

5 files changed

+94
-41
lines changed

5 files changed

+94
-41
lines changed

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

Lines changed: 46 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,36 @@ 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+
val inConstructor =
79+
(method.isPrimaryConstructor || inAnonFunInCtor)
80+
&& ctx.owner.enclosingClass == sym.owner
81+
val noField =
82+
inConstructor
83+
&& (sym.is(ParamAccessor) || seenPrivateVals.contains(sym))
84+
// used inside constructor, accessed on this,
85+
// could use constructor argument instead, no need to retain field
86+
if !noField then
87+
retain()
88+
case _ =>
89+
retain()
8590

8691
override def transformIdent(tree: tpd.Ident)(using Context): tpd.Tree = {
8792
markUsedPrivateSymbols(tree)
@@ -184,6 +189,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
184189
transform(tree).changeOwnerAfter(prevOwner, constr.symbol, thisPhase)
185190
}
186191

192+
// mightBeDropped is trivially false for NoSymbol -> NoSymbol isRetained
187193
def isRetained(acc: Symbol) =
188194
!mightBeDropped(acc) || retainedPrivateVals(acc)
189195

@@ -209,30 +215,28 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
209215
}
210216
}
211217

212-
val dropped = mutable.Set[Symbol]()
218+
val dropped = mutable.Set.empty[Symbol]
213219

214220
// Split class body into statements that go into constructor and
215221
// definitions that are kept as members of the class.
216-
def splitStats(stats: List[Tree]): Unit = stats match {
217-
case stat :: stats1 =>
222+
def splitStats(stats: List[Tree]): Unit = stats match
223+
case stat :: stats =>
224+
val sym = stat.symbol
218225
stat match {
219-
case stat @ ValDef(name, tpt, _) if !stat.symbol.is(Lazy) && !stat.symbol.hasAnnotation(defn.ScalaStaticAnnot) =>
220-
val sym = stat.symbol
226+
case stat @ ValDef(name, tpt, _) if !sym.is(Lazy) && !sym.hasAnnotation(defn.ScalaStaticAnnot) =>
221227
if (isRetained(sym)) {
222228
if (!stat.rhs.isEmpty && !isWildcardArg(stat.rhs))
223229
constrStats += Assign(ref(sym), intoConstr(stat.rhs, sym)).withSpan(stat.span)
224230
clsStats += cpy.ValDef(stat)(rhs = EmptyTree)
225231
}
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
232+
else
233233
dropped += sym
234+
if !stat.rhs.isEmpty then
235+
sym.copySymDenotation(
236+
initFlags = sym.flags &~ Private,
237+
owner = constr.symbol).installAfter(thisPhase)
238+
constrStats += intoConstr(stat, sym)
234239
case stat @ DefDef(name, _, tpt, _) if stat.symbol.isGetter && !stat.symbol.is(Lazy) =>
235-
val sym = stat.symbol
236240
assert(isRetained(sym), sym)
237241
if sym.isConstExprFinalVal then
238242
if stat.rhs.isInstanceOf[Literal] then
@@ -271,9 +275,9 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
271275
case _ =>
272276
constrStats += intoConstr(stat, tree.symbol)
273277
}
274-
splitStats(stats1)
278+
splitStats(stats)
275279
case Nil =>
276-
}
280+
end splitStats
277281

278282
/** Check that we do not have both a private field with name `x` and a private field
279283
* with name `FieldName(x)`. These will map to the same JVM name and therefore cause
@@ -303,14 +307,15 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
303307
dropped += acc
304308
Nil
305309
}
306-
else if (!isRetained(acc.field)) { // It may happen for unit fields, tests/run/i6987.scala
310+
else if (acc.field.exists && !isRetained(acc.field)) { // It may happen for unit fields, tests/run/i6987.scala
307311
dropped += acc.field
308312
Nil
309313
}
310314
else {
311315
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)
316+
if param.hasAnnotation(defn.ConstructorOnlyAnnot) then
317+
val msg = em"${acc.name} is marked `@constructorOnly` but it is retained as a field in ${acc.owner}"
318+
report.error(msg, acc.srcPos)
314319
val target = if (acc.is(Method)) acc.field else acc
315320
if (!target.exists) Nil // this case arises when the parameter accessor is an alias
316321
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: 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):
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(_))

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
import util.Try
3+
4+
@main def Test =
5+
assert(Try(classOf[Leak].getDeclaredField("l")).isFailure)
6+
assert(classOf[Leak].getFields.length == 0)
7+
//classOf[Leak].getFields.map(_.getName).foreach(println) //DEBUG
8+
assert(classOf[C].getFields.length == 0)
9+
10+
class C:
11+
private val x = 42
12+
println(x)
13+
println(List(27).map(_ + x))

0 commit comments

Comments
 (0)