Skip to content

Commit 2ff15be

Browse files
authored
Merge pull request #9427 from sjrd/scalajs-reflective-calls
Scala.js: Support for reflective calls.
2 parents ec50bcf + d5e8831 commit 2ff15be

File tree

12 files changed

+409
-30
lines changed

12 files changed

+409
-30
lines changed

compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,9 +512,16 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder {
512512

513513
case ClazzTag =>
514514
val tp = toTypeKind(const.typeValue)
515-
// classOf[Int] is transformed to Integer.TYPE by ClassOf
516-
assert(!tp.isPrimitive, s"expected class type in classOf[T], found primitive type $tp")
517-
mnode.visitLdcInsn(tp.toASMType)
515+
if tp.isPrimitive then
516+
val boxedClass = boxedClassOfPrimitive(tp.asPrimitiveBType)
517+
mnode.visitFieldInsn(
518+
asm.Opcodes.GETSTATIC,
519+
boxedClass.internalName,
520+
"TYPE", // field name
521+
jlClassRef.descriptor
522+
)
523+
else
524+
mnode.visitLdcInsn(tp.toASMType)
518525

519526
case EnumTag =>
520527
val sym = const.symbolValue

compiler/src/dotty/tools/backend/jvm/CoreBTypes.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: DottyBackendInterface]](val bTyp
124124
lazy val jlStringBuilderRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuilder])
125125
lazy val jlStringBufferRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.StringBuffer])
126126
lazy val jlCharSequenceRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.CharSequence])
127+
lazy val jlClassRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.Class[_]])
127128
lazy val ThrowableReference : ClassBType = classBTypeFromSymbol(defn.ThrowableClass)
128129
lazy val jlCloneableReference : ClassBType = classBTypeFromSymbol(defn.JavaCloneableClass) // java/lang/Cloneable
129130
lazy val jlNPEReference : ClassBType = classBTypeFromSymbol(defn.NullPointerExceptionClass) // java/lang/NullPointerException
@@ -248,6 +249,7 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: DottyBackendInterface
248249
def jlStringBuilderRef : ClassBType = _coreBTypes.jlStringBuilderRef
249250
def jlStringBufferRef : ClassBType = _coreBTypes.jlStringBufferRef
250251
def jlCharSequenceRef : ClassBType = _coreBTypes.jlCharSequenceRef
252+
def jlClassRef : ClassBType = _coreBTypes.jlClassRef
251253
def ThrowableReference : ClassBType = _coreBTypes.ThrowableReference
252254
def jlCloneableReference : ClassBType = _coreBTypes.jlCloneableReference
253255
def jlNPEReference : ClassBType = _coreBTypes.jlNPEReference

compiler/src/dotty/tools/backend/jvm/scalaPrimitives.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ class DottyPrimitives(ictx: Context) {
397397
primitives.toMap
398398
}
399399

400+
def isPrimitive(sym: Symbol): Boolean =
401+
primitives.contains(sym)
402+
400403
def isPrimitive(fun: Tree): Boolean =
401404
given Context = ictx
402405
primitives.contains(fun.symbol)
@@ -407,4 +410,3 @@ class DottyPrimitives(ictx: Context) {
407410
case _ => true
408411
})
409412
}
410-

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

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,9 +791,9 @@ class JSCodeGen()(using genCtx: Context) {
791791
toIRType(param.info), mutable = false, rest = false)
792792
}
793793

794-
/*if (primitives.isPrimitive(sym)) {
794+
if (primitives.isPrimitive(sym)) {
795795
None
796-
} else*/ if (sym.is(Deferred)) {
796+
} else if (sym.is(Deferred)) {
797797
Some(js.MethodDef(js.MemberFlags.empty, methodName, originalName,
798798
jsParams, toIRType(patchedResultType(sym)), None)(
799799
OptimizerHints.empty, None))
@@ -2815,9 +2815,181 @@ 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+
genReflectiveCall(tree, isSelectDynamic = true)
2822+
case REFLECT_SELECTABLE_APPLYDYN =>
2823+
// scala.reflect.Selectable.applyDynamic
2824+
genReflectiveCall(tree, isSelectDynamic = false)
28182825
}
28192826
}
28202827

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

29993172
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
@@ -259,8 +259,15 @@ object JSEncoding {
259259
ClassName(sym1.javaClassName)
260260
}
261261

262-
def toIRType(tp: Type)(using Context): jstpe.Type = {
262+
def toIRTypeAndTypeRef(tp: Type)(using Context): (jstpe.Type, jstpe.TypeRef) = {
263263
val typeRefInternal = toTypeRefInternal(tp)
264+
(toIRTypeInternal(typeRefInternal), typeRefInternal._1)
265+
}
266+
267+
def toIRType(tp: Type)(using Context): jstpe.Type =
268+
toIRTypeInternal(toTypeRefInternal(tp))
269+
270+
private def toIRTypeInternal(typeRefInternal: (jstpe.TypeRef, Symbol))(using Context): jstpe.Type = {
264271
typeRefInternal._1 match {
265272
case jstpe.PrimRef(irTpe) =>
266273
irTpe

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

Lines changed: 10 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
@@ -59,6 +62,9 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) {
5962
override def getPrimitive(app: Apply, tpe: Type)(using Context): Int =
6063
jsPrimitives.getOrElse(app.fun.symbol, super.getPrimitive(app, tpe))
6164

65+
override def isPrimitive(sym: Symbol): Boolean =
66+
jsPrimitives.contains(sym) || super.isPrimitive(sym)
67+
6268
override def isPrimitive(fun: Tree): Boolean =
6369
jsPrimitives.contains(fun.symbol(using ictx)) || super.isPrimitive(fun)
6470

@@ -109,6 +115,9 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) {
109115

110116
addPrimitive(defn.throwMethod, THROW)
111117

118+
addPrimitive(jsdefn.ReflectSelectable_selectDynamic, REFLECT_SELECTABLE_SELECTDYN)
119+
addPrimitive(jsdefn.ReflectSelectable_applyDynamic, REFLECT_SELECTABLE_APPLYDYN)
120+
112121
primitives.toMap
113122
}
114123

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,20 +1155,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
11551155

11561156
/** A tree that corresponds to `Predef.classOf[$tp]` in source */
11571157
def clsOf(tp: Type)(using Context): Tree =
1158-
if ctx.erasedTypes then
1159-
def TYPE(module: TermSymbol) = ref(module).select(nme.TYPE_)
1160-
defn.scalaClassName(tp) match
1161-
case tpnme.Boolean => TYPE(defn.BoxedBooleanModule)
1162-
case tpnme.Byte => TYPE(defn.BoxedByteModule)
1163-
case tpnme.Short => TYPE(defn.BoxedShortModule)
1164-
case tpnme.Char => TYPE(defn.BoxedCharModule)
1165-
case tpnme.Int => TYPE(defn.BoxedIntModule)
1166-
case tpnme.Long => TYPE(defn.BoxedLongModule)
1167-
case tpnme.Float => TYPE(defn.BoxedFloatModule)
1168-
case tpnme.Double => TYPE(defn.BoxedDoubleModule)
1169-
case tpnme.Unit => TYPE(defn.BoxedUnitModule)
1170-
case _ =>
1171-
Literal(Constant(TypeErasure.erasure(tp)))
1158+
if ctx.erasedTypes && !tp.isRef(defn.UnitClass) then
1159+
Literal(Constant(TypeErasure.erasure(tp)))
11721160
else
11731161
Literal(Constant(tp))
11741162

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,8 @@ class Definitions {
11661166

11671167
@tu lazy val NotRuntimeClasses: Set[Symbol] = Set(AnyClass, AnyValClass, NullClass, NothingClass)
11681168

1169+
@tu lazy val SpecialClassTagClasses: Set[Symbol] = Set(UnitClass, AnyClass, AnyValClass)
1170+
11691171
/** Classes that are known not to have an initializer irrespective of
11701172
* whether NoInits is set. Note: FunctionXXLClass is in this set
11711173
* because if it is compiled by Scala2, it does not get a NoInit flag.

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
3636
val sym = tp.typeSymbol
3737
val classTag = ref(defn.ClassTagModule)
3838
val tag =
39-
if sym == defn.UnitClass
40-
|| sym == defn.AnyClass
41-
|| sym == defn.AnyValClass
42-
then
39+
if defn.SpecialClassTagClasses.contains(sym) then
4340
classTag.select(sym.name.toTermName)
4441
else
4542
classTag.select(nme.apply).appliedToType(tp).appliedTo(clsOf(erasure(tp)))
@@ -425,4 +422,4 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
425422
EmptyTree
426423
recur(specialHandlers)
427424

428-
end Synthesizer
425+
end Synthesizer

project/Build.scala

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

0 commit comments

Comments
 (0)