Skip to content

Commit c99b7e0

Browse files
committed
Scala.js: Support for reflective calls.
This is significantly different from the code in scalac. In scalac, reflective calls reach the back-end as dedicated `ApplyDynamic` nodes. In dotc however, they are expanded by the type-checker into user-level calls to method of `scala.reflect.Selectable`. We must therefore reverse-engineer the patterns of code produced by the compiler to construct SJSIR `Apply` nodes with reflective proxy method names. The big comment on `JSCodeGen.genReflectiveCall` explains the translation in detail.
1 parent e39be1c commit c99b7e0

File tree

6 files changed

+348
-5
lines changed

6 files changed

+348
-5
lines changed

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

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,9 +2815,179 @@ class JSCodeGen()(using genCtx: Context) {
28152815
js.ForIn(objVarDef.ref, keyVarIdent, NoOriginalName, {
28162816
js.JSFunctionApply(fVarDef.ref, List(keyVarRef))
28172817
}))
2818+
2819+
case REFLECT_SELECTABLE_SELECTDYN =>
2820+
// scala.reflect.Selectable.selectDynamic
2821+
if (currentClassSym.get == jsdefn.ReflectSelectableClass)
2822+
genNormalApply(tree, isStat)
2823+
else
2824+
genReflectiveCall(tree, isSelectDynamic = true)
2825+
case REFLECT_SELECTABLE_APPLYDYN =>
2826+
// scala.reflect.Selectable.applyDynamic
2827+
if (currentClassSym.get == jsdefn.ReflectSelectableClass)
2828+
genNormalApply(tree, isStat)
2829+
else
2830+
genReflectiveCall(tree, isSelectDynamic = false)
28182831
}
28192832
}
28202833

2834+
/** Gen the SJSIR for a reflective call.
2835+
*
2836+
* Reflective calls are calls to a structural type field or method that
2837+
* involve a reflective Selectable. They look like the following in source
2838+
* code:
2839+
* {{{
2840+
* import scala.reflect.Selectable.reflectiveSelectable
2841+
*
2842+
* type Structural = {
2843+
* val foo: Int
2844+
* def bar(x: Int, y: String): String
2845+
* }
2846+
*
2847+
* val structural: Structural = new {
2848+
* val foo: Int = 5
2849+
* def bar(x: Int, y: String): String = x.toString + y
2850+
* }
2851+
*
2852+
* structural.foo
2853+
* structural.bar(6, "hello")
2854+
* }}}
2855+
*
2856+
* After expansion by the Scala 3 rules for structural member selections and
2857+
* calls, they look like
2858+
*
2859+
* {{{
2860+
* reflectiveSelectable(structural).selectDynamic("foo")
2861+
* reflectiveSelectable(structural).applyDynamic("bar",
2862+
* ClassTag(classOf[Int]), ClassTag(classOf[String])
2863+
* )(
2864+
* 6, "hello"
2865+
* )
2866+
* }}}
2867+
*
2868+
* and eventually reach the back-end as
2869+
*
2870+
* {{{
2871+
* reflectiveSelectable(structural).selectDynamic("foo") // same as above
2872+
* reflectiveSelectable(structural).applyDynamic("bar",
2873+
* wrapRefArray([ ClassTag(classOf[Int]), ClassTag(classOf[String]) : ClassTag ]
2874+
* )(
2875+
* genericWrapArray([ Int.box(6), "hello" : Object ])
2876+
* )
2877+
* }}}
2878+
*
2879+
* If we use the deprecated `import scala.language.reflectiveCalls`, the
2880+
* wrapper for the receiver `structural` are the following instead:
2881+
*
2882+
* {{{
2883+
* reflectiveSelectableFromLangReflectiveCalls(structural)(
2884+
* using scala.languageFeature.reflectiveCalls)
2885+
* }}}
2886+
*
2887+
* (in which case we don't care about the contextual argument).
2888+
*
2889+
* In SJSIR, they must be encoded as follows:
2890+
*
2891+
* {{{
2892+
* structural.foo;R()
2893+
* structural.bar;I;Ljava.lang.String;R(
2894+
* Int.box(6).asInstanceOf[int],
2895+
* "hello".asInstanceOf[java.lang.String]
2896+
* )
2897+
* }}}
2898+
*
2899+
* This means that we must deconstruct the elaborated calls to recover:
2900+
*
2901+
* - the original receiver `structural`
2902+
* - the method name as a compile-time string `foo` or `bar`
2903+
* - the `tp: Type`s that have been wrapped in `ClassTag(classOf[tp])`, as a
2904+
* compile-time List[Type], from which we'll derive `jstpe.Type`s for the
2905+
* `asInstanceOf`s and `jstpe.TypeRef`s for the `MethodName.reflectiveProxy`
2906+
* - the actual arguments as a compile-time `List[Tree]`
2907+
*
2908+
* Virtually all of the code in `genReflectiveCall` deals with recovering
2909+
* those elements. Constructing the IR Tree is the easy part after that.
2910+
*/
2911+
private def genReflectiveCall(tree: Apply, isSelectDynamic: Boolean): js.Tree = {
2912+
implicit val pos = tree.span
2913+
val Apply(fun @ Select(receiver0, _), args) = tree
2914+
2915+
/* Extract the real receiver, which is the first argument to one of the
2916+
* implicit conversions scala.reflect.Selectable.reflectiveSelectable or
2917+
* scala.Selectable.reflectiveSelectableFromLangReflectiveCalls.
2918+
*/
2919+
val receiver = receiver0 match {
2920+
case Apply(fun1, receiver :: _)
2921+
if fun1.symbol == jsdefn.ReflectSelectable_reflectiveSelectable ||
2922+
fun1.symbol == jsdefn.Selectable_reflectiveSelectableFromLangReflectiveCalls =>
2923+
genExpr(receiver)
2924+
2925+
case _ =>
2926+
report.error(
2927+
"The receiver of Selectable.selectDynamic or Selectable.applyDynamic " +
2928+
"must be a call to the (implicit) method scala.reflect.Selectable.reflectiveSelectable. " +
2929+
"Other uses are not supported in Scala.js.",
2930+
tree.sourcePos)
2931+
js.Undefined()
2932+
}
2933+
2934+
// Extract the method name as a String
2935+
val methodNameStr = args.head match {
2936+
case Literal(Constants.Constant(name: String)) =>
2937+
name
2938+
case _ =>
2939+
report.error(
2940+
"The method name given to Selectable.selectDynamic or Selectable.applyDynamic " +
2941+
"must be a literal string. " +
2942+
"Other uses are not supported in Scala.js.",
2943+
args.head.sourcePos)
2944+
"erroneous"
2945+
}
2946+
2947+
val (formalParamTypeRefs, actualArgs) = if (isSelectDynamic) {
2948+
(Nil, Nil)
2949+
} else {
2950+
// Extract the param type refs and actual args from the 2nd and 3rd argument to applyDynamic
2951+
args.tail match {
2952+
case WrapArray(classTagsArray: JavaSeqLiteral) :: WrapArray(actualArgsAnyArray: JavaSeqLiteral) :: Nil =>
2953+
// Extract jstpe.Type's and jstpe.TypeRef's from the ClassTag.apply(_) trees
2954+
val formalParamTypesAndTypeRefs = classTagsArray.elems.map {
2955+
case Apply(fun, Literal(const) :: Nil)
2956+
if fun.symbol == defn.ClassTagModule_apply && const.tag == Constants.ClazzTag =>
2957+
toIRTypeAndTypeRef(const.typeValue)
2958+
case classTag =>
2959+
report.error(
2960+
"The ClassTags passed to Selectable.applyDynamic must be " +
2961+
"literal ClassTag(classOf[T]) expressions " +
2962+
"(typically compiler-generated). " +
2963+
"Other uses are not supported in Scala.js.",
2964+
classTag.sourcePos)
2965+
(jstpe.AnyType, jstpe.ClassRef(jsNames.ObjectClass))
2966+
}
2967+
2968+
// Gen the actual args, downcasting them to the formal param types
2969+
val actualArgs = actualArgsAnyArray.elems.zip(formalParamTypesAndTypeRefs).map {
2970+
(actualArgAny, formalParamTypeAndTypeRef) =>
2971+
val genActualArgAny = genExpr(actualArgAny)
2972+
js.AsInstanceOf(genActualArgAny, formalParamTypeAndTypeRef._1)(genActualArgAny.pos)
2973+
}
2974+
2975+
(formalParamTypesAndTypeRefs.map(_._2), actualArgs)
2976+
2977+
case _ =>
2978+
report.error(
2979+
"Passing the varargs of Selectable.applyDynamic with `: _*` " +
2980+
"is not supported in Scala.js.",
2981+
tree.sourcePos)
2982+
(Nil, Nil)
2983+
}
2984+
}
2985+
2986+
val methodName = MethodName.reflectiveProxy(methodNameStr, formalParamTypeRefs)
2987+
2988+
js.Apply(js.ApplyFlags.empty, receiver, js.MethodIdent(methodName), actualArgs)(jstpe.AnyType)
2989+
}
2990+
28212991
/** Gen actual actual arguments to Scala method call.
28222992
* Returns a list of the transformed arguments.
28232993
*
@@ -2992,8 +3162,9 @@ class JSCodeGen()(using genCtx: Context) {
29923162
lazy val isWrapArray: Set[Symbol] = {
29933163
val names0 = defn.ScalaValueClasses().map(sym => nme.wrapXArray(sym.name))
29943164
val names1 = names0 ++ Set(nme.wrapRefArray, nme.genericWrapArray)
2995-
val names2 = names1.map(defn.ScalaPredefModule.requiredMethod(_))
2996-
names2.toSet
3165+
val symsInPredef = names1.map(defn.ScalaPredefModule.requiredMethod(_))
3166+
val symsInScalaRunTime = names1.map(defn.ScalaRuntimeModule.requiredMethod(_))
3167+
(symsInPredef ++ symsInScalaRunTime).toSet
29973168
}
29983169

29993170
def unapply(tree: Apply): Option[Tree] = tree match {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ final class JSDefinitions()(using Context) {
173173
@threadUnsafe lazy val Reflect_registerInstantiatableClassR = ReflectModule.requiredMethodRef("registerInstantiatableClass")
174174
def Reflect_registerInstantiatableClass(using Context) = Reflect_registerInstantiatableClassR.symbol
175175

176+
@threadUnsafe lazy val ReflectSelectableType: TypeRef = requiredClassRef("scala.reflect.Selectable")
177+
def ReflectSelectableClass(using Context) = ReflectSelectableType.symbol.asClass
178+
@threadUnsafe lazy val ReflectSelectable_selectDynamicR = ReflectSelectableClass.requiredMethodRef("selectDynamic")
179+
def ReflectSelectable_selectDynamic(using Context) = ReflectSelectable_selectDynamicR.symbol
180+
@threadUnsafe lazy val ReflectSelectable_applyDynamicR = ReflectSelectableClass.requiredMethodRef("applyDynamic")
181+
def ReflectSelectable_applyDynamic(using Context) = ReflectSelectable_applyDynamicR.symbol
182+
183+
@threadUnsafe lazy val ReflectSelectableModuleRef = requiredModuleRef("scala.reflect.Selectable")
184+
def ReflectSelectableModule(using Context) = ReflectSelectableModuleRef.symbol
185+
@threadUnsafe lazy val ReflectSelectable_reflectiveSelectableR = ReflectSelectableModule.requiredMethodRef("reflectiveSelectable")
186+
def ReflectSelectable_reflectiveSelectable(using Context) = ReflectSelectable_reflectiveSelectableR.symbol
187+
188+
@threadUnsafe lazy val SelectableModuleRef = requiredModuleRef("scala.Selectable")
189+
def SelectableModule(using Context) = SelectableModuleRef.symbol
190+
@threadUnsafe lazy val Selectable_reflectiveSelectableFromLangReflectiveCallsR = SelectableModule.requiredMethodRef("reflectiveSelectableFromLangReflectiveCalls")
191+
def Selectable_reflectiveSelectableFromLangReflectiveCalls(using Context) = Selectable_reflectiveSelectableFromLangReflectiveCallsR.symbol
192+
176193
private var allRefClassesCache: Set[Symbol] = _
177194
def allRefClasses(using Context): Set[Symbol] = {
178195
if (allRefClassesCache == null) {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,15 @@ object JSEncoding {
253253
}
254254
}
255255

256-
def toIRType(tp: Type)(using Context): jstpe.Type = {
256+
def toIRTypeAndTypeRef(tp: Type)(using Context): (jstpe.Type, jstpe.TypeRef) = {
257257
val typeRefInternal = toTypeRefInternal(tp)
258+
(toIRTypeInternal(typeRefInternal), typeRefInternal._1)
259+
}
260+
261+
def toIRType(tp: Type)(using Context): jstpe.Type =
262+
toIRTypeInternal(toTypeRefInternal(tp))
263+
264+
private def toIRTypeInternal(typeRefInternal: (jstpe.TypeRef, Symbol))(using Context): jstpe.Type = {
258265
typeRefInternal._1 match {
259266
case jstpe.PrimRef(irTpe) =>
260267
irTpe

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ object JSPrimitives {
4040

4141
final val THROW = DEBUGGER + 1
4242

43-
final val LastJSPrimitiveCode = THROW
43+
final val REFLECT_SELECTABLE_SELECTDYN = THROW + 1 // scala.reflect.Selectable.selectDynamic
44+
final val REFLECT_SELECTABLE_APPLYDYN = REFLECT_SELECTABLE_SELECTDYN + 1 // scala.reflect.Selectable.applyDynamic
45+
46+
final val LastJSPrimitiveCode = REFLECT_SELECTABLE_APPLYDYN
4447

4548
def isJSPrimitive(code: Int): Boolean =
4649
code >= FirstJSPrimitiveCode && code <= LastJSPrimitiveCode
@@ -109,6 +112,9 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) {
109112

110113
addPrimitive(defn.throwMethod, THROW)
111114

115+
addPrimitive(jsdefn.ReflectSelectable_selectDynamic, REFLECT_SELECTABLE_SELECTDYN)
116+
addPrimitive(jsdefn.ReflectSelectable_applyDynamic, REFLECT_SELECTABLE_APPLYDYN)
117+
112118
primitives.toMap
113119
}
114120

project/Build.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ object Build {
10591059
++ (dir / "shared/src/test/scala/org/scalajs/testsuite/niocharset" ** (("*.scala": FileFilter) -- "BaseCharsetTest.scala" -- "Latin1Test.scala" -- "USASCIITest.scala" -- "UTF16Test.scala" -- "UTF8Test.scala")).get
10601060
++ (dir / "shared/src/test/scala/org/scalajs/testsuite/scalalib" ** (("*.scala": FileFilter) -- "ArrayBuilderTest.scala" -- "ClassTagTest.scala" -- "EnumerationTest.scala" -- "SymbolTest.scala")).get
10611061
++ (dir / "shared/src/test/require-sam" ** "*.scala").get
1062-
++ (dir / "shared/src/test/require-jdk8/org/scalajs/testsuite/compiler" ** (("*.scala": FileFilter) -- "DefaultMethodsTest.scala")).get
1062+
++ (dir / "shared/src/test/require-jdk8/org/scalajs/testsuite/compiler" ** "*.scala").get
10631063
++ (dir / "shared/src/test/require-jdk8/org/scalajs/testsuite/javalib/lang" ** "*.scala").get
10641064
++ (dir / "shared/src/test/require-jdk8/org/scalajs/testsuite/javalib/util" ** (("*.scala": FileFilter) -- "CollectionsOnCopyOnWriteArrayListTestOnJDK8.scala")).get
10651065
++ (dir / "shared/src/test/require-jdk7/org/scalajs/testsuite/javalib/io" ** "*.scala").get

0 commit comments

Comments
 (0)