Skip to content

Commit 73ea507

Browse files
committed
Make the dynamic methods of reflect.Selectable final.
By making them final, the Scala.js back-end can process them not only for `reflectiveSelectable`, but for any custom subclass of `reflect.Selectable`. This removes its main limitation on Scala.js. We add `@inline` to the default implementation in `reflectiveSelectable` (which requires to make it a named class) so that the Scala.js optimizer can completely optimize it away at link time. On the JVM, this change has no effect.
1 parent 9015518 commit 73ea507

File tree

4 files changed

+83
-57
lines changed

4 files changed

+83
-57
lines changed

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

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3562,40 +3562,48 @@ class JSCodeGen()(using genCtx: Context) {
35623562
* )
35633563
* }}}
35643564
*
3565-
* and eventually reach the back-end as
3565+
* When the original `structural` value is already of a subtype of
3566+
* `scala.reflect.Selectable`, there is no conversion involved. There could
3567+
* also be any other arbitrary conversion, such as the deprecated bridge for
3568+
* Scala 2's `import scala.language.reflectiveCalls`. In general, the shape
3569+
* is therefore the following, for some `selectable: reflect.Selectable`:
35663570
*
35673571
* {{{
3568-
* reflectiveSelectable(structural).selectDynamic("foo") // same as above
3569-
* reflectiveSelectable(structural).applyDynamic("bar",
3570-
* wrapRefArray([ classOf[Int], classOf[String] : jl.Class ]
3572+
* selectable.selectDynamic("foo")
3573+
* selectable.applyDynamic("bar",
3574+
* classOf[Int], classOf[String]
35713575
* )(
3572-
* genericWrapArray([ Int.box(6), "hello" : Object ])
3576+
* 6, "hello"
35733577
* )
35743578
* }}}
35753579
*
3576-
* If we use the deprecated `import scala.language.reflectiveCalls`, the
3577-
* wrapper for the receiver `structural` are the following instead:
3580+
* and eventually reaches the back-end as
35783581
*
35793582
* {{{
3580-
* reflectiveSelectableFromLangReflectiveCalls(structural)(
3581-
* using scala.languageFeature.reflectiveCalls)
3583+
* selectable.selectDynamic("foo") // same as above
3584+
* selectable.applyDynamic("bar",
3585+
* wrapRefArray([ classOf[Int], classOf[String] : jl.Class ]
3586+
* )(
3587+
* genericWrapArray([ Int.box(6), "hello" : Object ])
3588+
* )
35823589
* }}}
35833590
*
3584-
* (in which case we don't care about the contextual argument).
3585-
*
35863591
* In SJSIR, they must be encoded as follows:
35873592
*
35883593
* {{{
3589-
* structural.foo;R()
3590-
* structural.bar;I;Ljava.lang.String;R(
3594+
* selectable.selectedValue;O().foo;R()
3595+
* selectable.selectedValue;O().bar;I;Ljava.lang.String;R(
35913596
* Int.box(6).asInstanceOf[int],
35923597
* "hello".asInstanceOf[java.lang.String]
35933598
* )
35943599
* }}}
35953600
*
3601+
* where `selectedValue;O()` is declared in `scala.reflect.Selectable` and
3602+
* holds the actual instance on which to perform the reflective operations.
3603+
* For the typical use case from the first snippet, it returns `structural`.
3604+
*
35963605
* This means that we must deconstruct the elaborated calls to recover:
35973606
*
3598-
* - the original receiver `structural`
35993607
* - the method name as a compile-time string `foo` or `bar`
36003608
* - the `tp: Type`s that have been wrapped in `classOf[tp]`, as a
36013609
* compile-time List[Type], from which we'll derive `jstpe.Type`s for the
@@ -3607,26 +3615,10 @@ class JSCodeGen()(using genCtx: Context) {
36073615
*/
36083616
private def genReflectiveCall(tree: Apply, isSelectDynamic: Boolean): js.Tree = {
36093617
implicit val pos = tree.span
3610-
val Apply(fun @ Select(receiver0, _), args) = tree
3618+
val Apply(fun @ Select(receiver, _), args) = tree
36113619

3612-
/* Extract the real receiver, which is the first argument to one of the
3613-
* implicit conversions scala.reflect.Selectable.reflectiveSelectable or
3614-
* scala.Selectable.reflectiveSelectableFromLangReflectiveCalls.
3615-
*/
3616-
val receiver = receiver0 match {
3617-
case Apply(fun1, receiver :: _)
3618-
if fun1.symbol == jsdefn.ReflectSelectable_reflectiveSelectable ||
3619-
fun1.symbol == jsdefn.Selectable_reflectiveSelectableFromLangReflectiveCalls =>
3620-
genExpr(receiver)
3621-
3622-
case _ =>
3623-
report.error(
3624-
"The receiver of Selectable.selectDynamic or Selectable.applyDynamic " +
3625-
"must be a call to the (implicit) method scala.reflect.Selectable.reflectiveSelectable. " +
3626-
"Other uses are not supported in Scala.js.",
3627-
tree.sourcePos)
3628-
js.Undefined()
3629-
}
3620+
val selectedValueTree = js.Apply(js.ApplyFlags.empty, genExpr(receiver),
3621+
js.MethodIdent(selectedValueMethodName), Nil)(jstpe.AnyType)
36303622

36313623
// Extract the method name as a String
36323624
val methodNameStr = args.head match {
@@ -3682,7 +3674,7 @@ class JSCodeGen()(using genCtx: Context) {
36823674

36833675
val methodName = MethodName.reflectiveProxy(methodNameStr, formalParamTypeRefs)
36843676

3685-
js.Apply(js.ApplyFlags.empty, receiver, js.MethodIdent(methodName), actualArgs)(jstpe.AnyType)
3677+
js.Apply(js.ApplyFlags.empty, selectedValueTree, js.MethodIdent(methodName), actualArgs)(jstpe.AnyType)
36863678
}
36873679

36883680
/** Gen actual actual arguments to Scala method call.
@@ -4296,10 +4288,13 @@ object JSCodeGen {
42964288
private val NullPointerExceptionClass = ClassName("java.lang.NullPointerException")
42974289
private val JSObjectClassName = ClassName("scala.scalajs.js.Object")
42984290

4291+
private val ObjectClassRef = jstpe.ClassRef(ir.Names.ObjectClass)
4292+
42994293
private val newSimpleMethodName = SimpleMethodName("new")
43004294

4301-
private val ObjectArgConstructorName =
4302-
MethodName.constructor(List(jstpe.ClassRef(ir.Names.ObjectClass)))
4295+
private val selectedValueMethodName = MethodName("selectedValue", Nil, ObjectClassRef)
4296+
4297+
private val ObjectArgConstructorName = MethodName.constructor(List(ObjectClassRef))
43034298

43044299
private val thisOriginalName = OriginalName("this")
43054300

library/src/scala/reflect/Selectable.scala

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package scala.reflect
22

33
/** A class that implements structural selections using Java reflection.
4+
*
45
* It can be used as a supertrait of a class or be made available
56
* as an implicit conversion via `reflectiveSelectable`.
7+
*
8+
* In Scala.js, it is implemented using a separate Scala.js-specific
9+
* mechanism, since Java reflection is not available.
610
*/
711
trait Selectable extends scala.Selectable:
812

@@ -12,8 +16,9 @@ trait Selectable extends scala.Selectable:
1216
*/
1317
protected def selectedValue: Any = this
1418

19+
// The Scala.js codegen relies on this method being final for correctness
1520
/** Select member with given name */
16-
def selectDynamic(name: String): Any =
21+
final def selectDynamic(name: String): Any =
1722
val rcls = selectedValue.getClass
1823
try
1924
val fld = rcls.getField(name)
@@ -22,12 +27,13 @@ trait Selectable extends scala.Selectable:
2227
catch case ex: NoSuchFieldException =>
2328
applyDynamic(name)()
2429

30+
// The Scala.js codegen relies on this method being final for correctness
2531
/** Select method and apply to arguments.
2632
* @param name The name of the selected method
2733
* @param paramTypes The class tags of the selected method's formal parameter types
2834
* @param args The arguments to pass to the selected method
2935
*/
30-
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any =
36+
final def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any =
3137
val rcls = selectedValue.getClass
3238
val mth = rcls.getMethod(name, paramTypes: _*)
3339
ensureAccessible(mth)
@@ -39,4 +45,7 @@ object Selectable:
3945
* such that structural selections are performed on that value.
4046
*/
4147
implicit def reflectiveSelectable(x: Any): Selectable =
42-
new Selectable { override val selectedValue = x }
48+
new DefaultSelectable(x)
49+
50+
@inline // important for Scala.js
51+
private final class DefaultSelectable(override protected val selectedValue: Any) extends Selectable

tests/neg-scalajs/reflective-calls.scala

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,34 @@ object Test {
66
* ensuring that the error cases we test are actually testing the right things.
77
*/
88
def sanityCheck(): Unit = {
9-
val receiver: Any = ???
10-
reflectiveSelectable(receiver).selectDynamic("foo") // OK
11-
reflectiveSelectable(receiver).applyDynamic("foo")() // OK
12-
reflectiveSelectable(receiver).applyDynamic("foo", classOf[String], classOf[List[_]])("bar", Nil) // OK
13-
}
14-
15-
def badReceider(): Unit = {
169
val receiver: ReflectSel = ???
17-
receiver.selectDynamic("foo") // error
18-
receiver.applyDynamic("foo")() // error
10+
receiver.selectDynamic("foo") // OK
11+
receiver.applyDynamic("foo")() // OK
12+
receiver.applyDynamic("foo", classOf[String], classOf[List[_]])("bar", Nil) // OK
1913
}
2014

2115
def nonLiteralMethodName(): Unit = {
22-
val receiver: Any = ???
16+
val receiver: ReflectSel = ???
2317
val methodName: String = "foo"
24-
reflectiveSelectable(receiver).selectDynamic(methodName) // error
25-
reflectiveSelectable(receiver).applyDynamic(methodName)() // error
18+
receiver.selectDynamic(methodName) // error
19+
receiver.applyDynamic(methodName)() // error
2620
}
2721

2822
def nonLiteralClassOf(): Unit = {
29-
val receiver: Any = ???
23+
val receiver: ReflectSel = ???
3024
val myClassOf: Class[String] = classOf[String]
31-
reflectiveSelectable(receiver).applyDynamic("foo", myClassOf, classOf[List[_]])("bar", Nil) // error
25+
receiver.applyDynamic("foo", myClassOf, classOf[List[_]])("bar", Nil) // error
3226
}
3327

3428
def classOfVarArgs(): Unit = {
35-
val receiver: Any = ???
29+
val receiver: ReflectSel = ???
3630
val classOfs: List[Class[_]] = List(classOf[String], classOf[List[_]])
37-
reflectiveSelectable(receiver).applyDynamic("foo", classOfs: _*)("bar", Nil) // error
31+
receiver.applyDynamic("foo", classOfs: _*)("bar", Nil) // error
3832
}
3933

4034
def argsVarArgs(): Unit = {
41-
val receiver: Any = ???
35+
val receiver: ReflectSel = ???
4236
val args: List[Any] = List("bar", Nil)
43-
reflectiveSelectable(receiver).applyDynamic("foo", classOf[String], classOf[List[_]])(args: _*) // error
37+
receiver.applyDynamic("foo", classOf[String], classOf[List[_]])(args: _*) // error
4438
}
4539
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.scalajs.testsuite.compiler
2+
3+
import scala.reflect.Selectable
4+
5+
import org.junit.Assert._
6+
import org.junit.Test
7+
8+
class CustomSelectableTestScala3 {
9+
import CustomSelectableTestScala3._
10+
11+
@Test def selectField(): Unit = {
12+
val obj: Selectable { val x: Int } = new CustomSelectable(42)
13+
assertEquals(47, obj.x)
14+
}
15+
16+
@Test def callMethod(): Unit = {
17+
val obj: Selectable { def foo(x: Int, y: String): String } = new CustomSelectable(42)
18+
assertEquals("3 bar 42", obj.foo(3, "bar"))
19+
}
20+
}
21+
22+
object CustomSelectableTestScala3 {
23+
class CustomSelectable(param: Int) extends Selectable {
24+
val x: Int = 5 + param
25+
26+
def foo(x: Int, y: String): String = s"$x $y $param"
27+
}
28+
}

0 commit comments

Comments
 (0)