Skip to content

Commit 96d4ccd

Browse files
authored
Fix #14168: Do not elide fields required for Scala.js interop semantics. (#16187)
2 parents bbeb20d + 49b7cda commit 96d4ccd

File tree

4 files changed

+138
-3
lines changed

4 files changed

+138
-3
lines changed

compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3144,7 +3144,23 @@ class JSCodeGen()(using genCtx: Context) {
31443144
val tpe = atPhase(elimErasedValueTypePhase) {
31453145
sym.info.finalResultType
31463146
}
3147-
unbox(boxedResult, tpe)
3147+
if (tpe.isRef(defn.BoxedUnitClass) && sym.isGetter) {
3148+
/* Work around to reclaim Scala 2 erasure behavior, assumed by the test
3149+
* NonNativeJSTypeTest.defaultValuesForFields.
3150+
* Scala 2 erases getters of `Unit`-typed fields as returning `Unit`
3151+
* (not `BoxedUnit`). Therefore, when called in expression position,
3152+
* the call site introduces an explicit `BoxedUnit.UNIT`. Even if the
3153+
* field has not been initialized at all (with `= _`), this results in
3154+
* an actual `()` value.
3155+
* In Scala 3, the same pattern returns `null`, as a `BoxedUnit`, so we
3156+
* introduce here an explicit `()` value.
3157+
* TODO We should remove this branch if the upstream test is updated
3158+
* not to assume such a strict interpretation of erasure.
3159+
*/
3160+
js.Block(boxedResult, js.Undefined())
3161+
} else {
3162+
unbox(boxedResult, tpe)
3163+
}
31483164
}
31493165
}
31503166

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ import Flags._
1616
import Decorators._
1717
import StdNames.nme
1818

19+
import sjs.JSSymUtils._
20+
1921
import util.Store
2022

23+
import dotty.tools.backend.sjs.JSDefinitions.jsdefn
24+
2125
object Memoize {
2226
val name: String = "memoize"
2327
val description: String = "add private fields to getters and setters"
@@ -142,14 +146,30 @@ class Memoize extends MiniPhase with IdentityDenotTransformer { thisPhase =>
142146
}
143147

144148
if sym.is(Accessor, butNot = NoFieldNeeded) then
149+
/* Tests whether the semantics of Scala.js require a field for this symbol, irrespective of any
150+
* optimization we think we can do. This is the case if one of the following is true:
151+
* - it is a member of a JS type, since it needs to be visible as a JavaScript field
152+
* - is is exported as static member of the companion class, since it needs to be visible as a JavaScript static field
153+
* - it is exported to the top-level, since that can only be done as a true top-level variable, i.e., a field
154+
*/
155+
def sjsNeedsField: Boolean =
156+
ctx.settings.scalajs.value && (
157+
sym.owner.isJSType
158+
|| sym.hasAnnotation(jsdefn.JSExportTopLevelAnnot)
159+
|| sym.hasAnnotation(jsdefn.JSExportStaticAnnot)
160+
)
161+
145162
def adaptToField(field: Symbol, tree: Tree): Tree =
146163
if (tree.isEmpty) tree else tree.ensureConforms(field.info.widen)
147164

148165
def isErasableBottomField(field: Symbol, cls: Symbol): Boolean =
149-
!field.isVolatile && ((cls eq defn.NothingClass) || (cls eq defn.NullClass) || (cls eq defn.BoxedUnitClass))
166+
!field.isVolatile
167+
&& ((cls eq defn.NothingClass) || (cls eq defn.NullClass) || (cls eq defn.BoxedUnitClass))
168+
&& !sjsNeedsField
150169

151170
if sym.isGetter then
152-
val constantFinalVal = sym.isAllOf(Accessor | Final, butNot = Mutable) && tree.rhs.isInstanceOf[Literal]
171+
val constantFinalVal =
172+
sym.isAllOf(Accessor | Final, butNot = Mutable) && tree.rhs.isInstanceOf[Literal] && !sjsNeedsField
153173
if constantFinalVal then
154174
// constant final vals do not need to be transformed at all, and do not need a field
155175
tree
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import scala.compiletime.uninitialized
2+
3+
object Test:
4+
def main(args: Array[String]): Unit =
5+
val foo = new Foo
6+
assertEquals(0, foo.int)
7+
assertEquals(false, foo.bool)
8+
assertEquals(null, foo.str)
9+
assertEquals((), foo.unit)
10+
assertEquals(ValueClass(0), foo.vc)
11+
end main
12+
13+
def assertEquals(expected: Any, actual: Any): Unit =
14+
assert(expected == actual)
15+
16+
class Foo:
17+
var int: Int = uninitialized
18+
var bool: Boolean = uninitialized
19+
var str: String = uninitialized
20+
var unit: Unit = uninitialized
21+
var vc: ValueClass = uninitialized
22+
end Foo
23+
24+
class ValueClass(val i: Int) extends AnyVal
25+
end Test

tests/sjs-junit/test/org/scalajs/testsuite/compiler/RegressionTestScala3.scala

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@ package org.scalajs.testsuite.compiler
33
import org.junit.Assert.*
44
import org.junit.Test
55

6+
import scala.concurrent.ExecutionContext.Implicits.{global => globalEc}
7+
import scala.concurrent.Future
8+
69
import scala.scalajs.js
710
import scala.scalajs.js.annotation._
811

12+
import org.scalajs.junit.async._
13+
14+
import org.scalajs.testsuite.jsinterop.ExportLoopback
15+
import org.scalajs.testsuite.utils.Platform._
16+
917
class RegressionTestScala3 {
1018
import RegressionTestScala3.*
1119

@@ -90,6 +98,39 @@ class RegressionTestScala3 {
9098

9199
assertEquals(5, Issue16173.bar1())
92100
}
101+
102+
@Test def mandatoryFieldsForSJSSemanticsInNonNativeJSClassIssue14168(): Unit = {
103+
val nonNativeJS = new Issue14168.NonNativeJSClass().asInstanceOf[js.Dynamic]
104+
assertEquals("string", nonNativeJS.stringField)
105+
assertEquals(null, nonNativeJS.nullField)
106+
assertEquals((), nonNativeJS.unitField)
107+
assertEquals(true, nonNativeJS.hasOwnProperty("unitField"))
108+
}
109+
110+
@Test def mandatoryFieldsForSJSSemanticsInStaticExportsIssue14168(): Unit = {
111+
val staticExports = js.constructorOf[Issue14168.StaticExports]
112+
assertEquals("string", staticExports.stringField)
113+
assertEquals(null, staticExports.nullField)
114+
assertEquals((), staticExports.unitField)
115+
assertEquals(true, staticExports.hasOwnProperty("unitField"))
116+
}
117+
118+
@Test def mandatoryFieldsForSJSSemanticsInTopLevelExportsIssue14168(): AsyncResult = await {
119+
if (isNoModule) {
120+
import js.Dynamic.global
121+
Future {
122+
assertEquals("string", global.RegressionTestScala3_Issue14168_stringField)
123+
assertEquals(null, global.RegressionTestScala3_Issue14168_nullField)
124+
assertEquals((), global.RegressionTestScala3_Issue14168_unitField)
125+
}
126+
} else {
127+
for (exports <- ExportLoopback.exportsNamespace) yield {
128+
assertEquals("string", exports.RegressionTestScala3_Issue14168_stringField)
129+
assertEquals(null, exports.RegressionTestScala3_Issue14168_nullField)
130+
assertEquals((), exports.RegressionTestScala3_Issue14168_unitField)
131+
}
132+
}
133+
}
93134
}
94135

95136
object RegressionTestScala3 {
@@ -174,6 +215,39 @@ object RegressionTestScala3 {
174215
@JSGlobal("RegressionTestScala3_Issue16173_bar")
175216
def bar1(): 5 = js.native
176217
}
218+
219+
object Issue14168 {
220+
class NonNativeJSClass extends js.Object {
221+
val stringField: "string" = "string"
222+
val nullField: Null = null
223+
val unitField: Unit = ()
224+
final val finalValField = "finalVal"
225+
}
226+
227+
class StaticExports extends js.Object
228+
229+
object StaticExports {
230+
@JSExportStatic
231+
val stringField: "string" = "string"
232+
@JSExportStatic
233+
val nullField: Null = null
234+
@JSExportStatic
235+
val unitField: Unit = ()
236+
@JSExportStatic
237+
final val finalValField = "finalVal"
238+
}
239+
240+
object TopLevelExports {
241+
@JSExportTopLevel("RegressionTestScala3_Issue14168_stringField")
242+
val stringField: "string" = "string"
243+
@JSExportTopLevel("RegressionTestScala3_Issue14168_nullField")
244+
val nullField: Null = null
245+
@JSExportTopLevel("RegressionTestScala3_Issue14168_unitField")
246+
val unitField: Unit = ()
247+
@JSExportTopLevel("RegressionTestScala3_Issue14168_finalValField")
248+
final val finalValField = "finalVal"
249+
}
250+
}
177251
}
178252

179253
// This class needs to be at the top-level, not in an object, to reproduce the issue

0 commit comments

Comments
 (0)