Skip to content

Commit ac2b53b

Browse files
committed
Fix #17549: Unify how Memoize and Constructors decide what fields need storing.
The Memoize and Constructors have to work together and agree on which `final val`s actually need storing in a field. Previously, they used slightly different criteria: one on the result type, and one on the rhs (with an additional Scala.js-specific eligibility condition). That discrepancy resulted in the crash/wrong codegen in the issue. We now unify both: we avoid a field if and only if all of the following apply: * it is a `final val`, * its result *type* is a `ConstantType`, and * it is eligible according to Scala.js semantics. In particular, there is no condition on the right-hand-side. We avoid a field even if the right-hand-side has side effects. The side effects are moved to the constructor regardless. --- This introduces both progressions and regressions in the amount of fields we generate. We can avoid fields even for side-effecting rhs'es, as long as the result type is constant. On the other hand, we now create a field for `final val`s with non-constant result type, even if the rhs is a literal. While the latter is a regression for Scala 3, it aligns with the behavior of Scala 2. It also has the nice benefit that whether or not a val has a field is now independent of its *implementation*, and only dependent on its *API*. Overall, I think this is a trade-off worth taking. We could reintroduce that optimization in the future (but in classes only; not in traits), if we really want to, although that would require dedicated code.
1 parent fe08f5f commit ac2b53b

File tree

8 files changed

+142
-46
lines changed

8 files changed

+142
-46
lines changed

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

+31-23
Original file line numberDiff line numberDiff line change
@@ -226,31 +226,39 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase =
226226
constrStats += intoConstr(stat, sym)
227227
} else
228228
dropped += sym
229-
case stat @ DefDef(name, _, tpt, _)
230-
if stat.symbol.isGetter && stat.symbol.owner.is(Trait) && !stat.symbol.is(Lazy) && !stat.symbol.isConstExprFinalVal =>
229+
case stat @ DefDef(name, _, tpt, _) if stat.symbol.isGetter && !stat.symbol.is(Lazy) =>
231230
val sym = stat.symbol
232231
assert(isRetained(sym), sym)
233-
if !stat.rhs.isEmpty && !isWildcardArg(stat.rhs) then
234-
/* !!! Work around #9390
235-
* This should really just be `sym.setter`. However, if we do that, we'll miss
236-
* setters for mixed in `private var`s. Even though the scope clearly contains the
237-
* setter symbol with the correct Name structure (since the `find` finds it),
238-
* `.decl(setterName)` used by `.setter` through `.accessorNamed` will *not* find it.
239-
* Could it be that the hash table of the `Scope` is corrupted?
240-
* We still try `sym.setter` first as an optimization, since it will work for all
241-
* public vars in traits and all (public or private) vars in classes.
242-
*/
243-
val symSetter =
244-
if sym.setter.exists then
245-
sym.setter
246-
else
247-
val setterName = sym.asTerm.name.setterName
248-
sym.owner.info.decls.find(d => d.is(Accessor) && d.name == setterName)
249-
val setter =
250-
if (symSetter.exists) symSetter
251-
else sym.accessorNamed(Mixin.traitSetterName(sym.asTerm))
252-
constrStats += Apply(ref(setter), intoConstr(stat.rhs, sym).withSpan(stat.span) :: Nil)
253-
clsStats += cpy.DefDef(stat)(rhs = EmptyTree)
232+
if sym.isConstExprFinalVal then
233+
if stat.rhs.isInstanceOf[Literal] then
234+
clsStats += stat
235+
else
236+
constrStats += intoConstr(stat.rhs, sym)
237+
clsStats += cpy.DefDef(stat)(rhs = Literal(sym.constExprFinalValConstantType.value).withSpan(stat.span))
238+
else if !sym.owner.is(Trait) then
239+
clsStats += stat
240+
else
241+
if !stat.rhs.isEmpty && !isWildcardArg(stat.rhs) then
242+
/* !!! Work around #9390
243+
* This should really just be `sym.setter`. However, if we do that, we'll miss
244+
* setters for mixed in `private var`s. Even though the scope clearly contains the
245+
* setter symbol with the correct Name structure (since the `find` finds it),
246+
* `.decl(setterName)` used by `.setter` through `.accessorNamed` will *not* find it.
247+
* Could it be that the hash table of the `Scope` is corrupted?
248+
* We still try `sym.setter` first as an optimization, since it will work for all
249+
* public vars in traits and all (public or private) vars in classes.
250+
*/
251+
val symSetter =
252+
if sym.setter.exists then
253+
sym.setter
254+
else
255+
val setterName = sym.asTerm.name.setterName
256+
sym.owner.info.decls.find(d => d.is(Accessor) && d.name == setterName)
257+
val setter =
258+
if (symSetter.exists) symSetter
259+
else sym.accessorNamed(Mixin.traitSetterName(sym.asTerm))
260+
constrStats += Apply(ref(setter), intoConstr(stat.rhs, sym).withSpan(stat.span) :: Nil)
261+
clsStats += cpy.DefDef(stat)(rhs = EmptyTree)
254262
case DefDef(nme.CONSTRUCTOR, ((outerParam @ ValDef(nme.OUTER, _, _)) :: _) :: Nil, _, _) =>
255263
clsStats += mapOuter(outerParam.symbol).transform(stat)
256264
case _: DefTree =>

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

+3-20
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import sjs.JSSymUtils._
2020

2121
import util.Store
2222

23-
import dotty.tools.backend.sjs.JSDefinitions.jsdefn
24-
2523
object Memoize {
2624
val name: String = "memoize"
2725
val description: String = "add private fields to getters and setters"
@@ -130,32 +128,17 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
130128
}
131129

132130
if sym.is(Accessor, butNot = NoFieldNeeded) then
133-
/* Tests whether the semantics of Scala.js require a field for this symbol, irrespective of any
134-
* optimization we think we can do. This is the case if one of the following is true:
135-
* - it is a member of a JS type, since it needs to be visible as a JavaScript field
136-
* - is is exported as static member of the companion class, since it needs to be visible as a JavaScript static field
137-
* - it is exported to the top-level, since that can only be done as a true top-level variable, i.e., a field
138-
*/
139-
def sjsNeedsField: Boolean =
140-
ctx.settings.scalajs.value && (
141-
sym.owner.isJSType
142-
|| sym.hasAnnotation(jsdefn.JSExportTopLevelAnnot)
143-
|| sym.hasAnnotation(jsdefn.JSExportStaticAnnot)
144-
)
145-
146131
def adaptToField(field: Symbol, tree: Tree): Tree =
147132
if (tree.isEmpty) tree else tree.ensureConforms(field.info.widen)
148133

149134
def isErasableBottomField(field: Symbol, cls: Symbol): Boolean =
150135
!field.isVolatile
151136
&& ((cls eq defn.NothingClass) || (cls eq defn.NullClass) || (cls eq defn.BoxedUnitClass))
152-
&& !sjsNeedsField
137+
&& !sym.sjsNeedsField
153138

154139
if sym.isGetter then
155-
val constantFinalVal =
156-
sym.isAllOf(Accessor | Final, butNot = Mutable) && tree.rhs.isInstanceOf[Literal] && !sjsNeedsField
157-
if constantFinalVal then
158-
// constant final vals do not need to be transformed at all, and do not need a field
140+
if sym.isConstExprFinalVal then
141+
// const-expr final vals do not need to be transformed at all, and do not need a field
159142
tree
160143
else
161144
val field = newField.asTerm

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

+23-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import Annotations.Annotation
1818
import Phases._
1919
import ast.tpd.Literal
2020

21+
import dotty.tools.dotc.transform.sjs.JSSymUtils.sjsNeedsField
22+
2123
import scala.annotation.tailrec
2224

2325
object SymUtils:
@@ -259,9 +261,29 @@ object SymUtils:
259261
self.owner.info.decl(fieldName).suchThat(!_.is(Method)).symbol
260262
}
261263

264+
/** Is this symbol a constant expression final val?
265+
*
266+
* This is the case if all of the following are true:
267+
*
268+
* - it is a `final val`,
269+
* - its result type is a `ConstantType`, and
270+
* - it does not need an explicit field because of Scala.js semantics (see `JSSymUtils.sjsNeedsField`).
271+
*
272+
* Constant expression final vals do not need an explicit field to store
273+
* their value. See the Memoize-Mixin-Constructors phase trio.
274+
*/
262275
def isConstExprFinalVal(using Context): Boolean =
263276
atPhaseNoLater(erasurePhase) {
264-
self.is(Final) && self.info.resultType.isInstanceOf[ConstantType]
277+
self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType]
278+
} && !self.sjsNeedsField
279+
280+
/** The `ConstantType` of a val known to be `isConstrExprFinalVal`.
281+
*
282+
* @pre `self.isConstantExprFinalVal` is true.
283+
*/
284+
def constExprFinalValConstantType(using Context): ConstantType =
285+
atPhaseNoLater(erasurePhase) {
286+
self.info.resultType.asInstanceOf[ConstantType]
265287
}
266288

267289
def isField(using Context): Boolean =

compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala

+17
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,23 @@ object JSSymUtils {
211211
}
212212
}
213213
}
214+
215+
/** Tests whether the semantics of Scala.js require a field for this symbol,
216+
* irrespective of any optimization we think we can do.
217+
*
218+
* This is the case if one of the following is true:
219+
*
220+
* - it is a member of a JS type, since it needs to be visible as a JavaScript field
221+
* - is is exported as static member of the companion class, since it needs to be visible as a JavaScript static field
222+
* - it is exported to the top-level, since that can only be done as a true top-level variable, i.e., a field
223+
*/
224+
def sjsNeedsField(using Context): Boolean =
225+
ctx.settings.scalajs.value && (
226+
sym.owner.isJSType
227+
|| sym.hasAnnotation(jsdefn.JSExportTopLevelAnnot)
228+
|| sym.hasAnnotation(jsdefn.JSExportStaticAnnot)
229+
)
230+
end sjsNeedsField
214231
}
215232

216233
private object JSUnaryOpMethodName {

tests/run/erased-inline-vals.scala

+34-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ class D:
1616
inline def x: Int = 5
1717
inline val y = 6
1818

19+
object SideEffects:
20+
val sideEffects = scala.collection.mutable.ListBuffer.empty[String]
21+
22+
trait E:
23+
final val a: 7 =
24+
SideEffects.sideEffects += "E.a"
25+
7
26+
final val b =
27+
SideEffects.sideEffects += "E.b"
28+
8
29+
end E
30+
31+
class F extends E:
32+
final val c: 9 =
33+
SideEffects.sideEffects += "F.c"
34+
9
35+
final val d =
36+
SideEffects.sideEffects += "F.d"
37+
10
38+
end F
39+
1940
@main def Test =
2041
val b: B = new B
2142
assert(b.x == 1)
@@ -37,12 +58,24 @@ class D:
3758
assert(d.x == 5)
3859
assert(d.y == 6)
3960

61+
val f: F = new F
62+
assert(SideEffects.sideEffects.toList == List("E.a", "E.b", "F.c", "F.d"))
63+
assert(f.a == 7)
64+
assert(f.b == 8)
65+
assert(f.c == 9)
66+
assert(f.d == 10)
4067

4168
assert(classOf[B].getDeclaredMethods.size == 2)
4269
assert(classOf[B].getDeclaredFields.isEmpty)
4370

4471
assert(classOf[C].getDeclaredMethods.size == 2)
45-
assert(classOf[C].getDeclaredFields.isEmpty)
72+
assert(classOf[C].getDeclaredFields.size == 1) // x, but not y
4673

4774
assert(classOf[D].getDeclaredMethods.isEmpty)
4875
assert(classOf[D].getFields.isEmpty)
76+
77+
assert(classOf[E].getDeclaredMethods.size == 5)
78+
assert(classOf[E].getDeclaredFields.isEmpty)
79+
80+
assert(classOf[F].getDeclaredMethods.size == 2)
81+
assert(classOf[F].getDeclaredFields.isEmpty)

tests/run/final-fields.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ T.f1
22
T.f2
33
T.f3
44
T.f4
5-
3 2 -3 -4
5+
3 2 3 -4
66
3
77
g

tests/run/i17549.check

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
T.x
2+
C.y
3+
1
4+
2
5+
1
6+
2

tests/run/i17549.scala

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
trait T:
2+
final val x: 1 =
3+
println("T.x")
4+
1
5+
end T
6+
7+
trait U:
8+
def x: Any
9+
def y: Any
10+
11+
class C extends T with U:
12+
final val y: 2 =
13+
println("C.y")
14+
2
15+
end C
16+
17+
object Test:
18+
def main(args: Array[String]): Unit =
19+
val c = new C
20+
println(c.x)
21+
println(c.y)
22+
23+
val u: U = c
24+
println(u.x)
25+
println(u.y)
26+
end main
27+
end Test

0 commit comments

Comments
 (0)