Skip to content

Make the dynamic methods of reflect.Selectable final. #10393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 61 additions & 76 deletions compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3199,26 +3199,24 @@ class JSCodeGen()(using genCtx: Context) {
}

/** Gen JS code for an asInstanceOf cast (for reference types only) */
private def genAsInstanceOf(value: js.Tree, to: Type)(
implicit pos: Position): js.Tree = {

val sym = to.typeSymbol
private def genAsInstanceOf(value: js.Tree, to: Type)(implicit pos: Position): js.Tree =
genAsInstanceOf(value, toIRType(to))

if (sym == defn.ObjectClass || sym.isJSType) {
/* asInstanceOf[Object] always succeeds, and
* asInstanceOf to a raw JS type is completely erased.
*/
value
} else if (sym == defn.NullClass) {
js.If(
js.BinaryOp(js.BinaryOp.===, value, js.Null()),
js.Null(),
genThrowClassCastException())(
jstpe.NullType)
} else if (sym == defn.NothingClass) {
js.Block(value, genThrowClassCastException())
} else {
js.AsInstanceOf(value, toIRType(to))
/** Gen JS code for an asInstanceOf cast (for reference types only) */
private def genAsInstanceOf(value: js.Tree, to: jstpe.Type)(implicit pos: Position): js.Tree = {
to match {
case jstpe.AnyType =>
value
case jstpe.NullType =>
js.If(
js.BinaryOp(js.BinaryOp.===, value, js.Null()),
js.Null(),
genThrowClassCastException())(
jstpe.NullType)
case jstpe.NothingType =>
js.Block(value, genThrowClassCastException())
case _ =>
js.AsInstanceOf(value, to)
}
}

Expand Down Expand Up @@ -3558,48 +3556,56 @@ class JSCodeGen()(using genCtx: Context) {
* {{{
* reflectiveSelectable(structural).selectDynamic("foo")
* reflectiveSelectable(structural).applyDynamic("bar",
* ClassTag(classOf[Int]), ClassTag(classOf[String])
* classOf[Int], classOf[String]
* )(
* 6, "hello"
* )
* }}}
*
* and eventually reach the back-end as
* When the original `structural` value is already of a subtype of
* `scala.reflect.Selectable`, there is no conversion involved. There could
* also be any other arbitrary conversion, such as the deprecated bridge for
* Scala 2's `import scala.language.reflectiveCalls`. In general, the shape
* is therefore the following, for some `selectable: reflect.Selectable`:
*
* {{{
* reflectiveSelectable(structural).selectDynamic("foo") // same as above
* reflectiveSelectable(structural).applyDynamic("bar",
* wrapRefArray([ ClassTag(classOf[Int]), ClassTag(classOf[String]) : ClassTag ]
* selectable.selectDynamic("foo")
* selectable.applyDynamic("bar",
* classOf[Int], classOf[String]
* )(
* genericWrapArray([ Int.box(6), "hello" : Object ])
* 6, "hello"
* )
* }}}
*
* If we use the deprecated `import scala.language.reflectiveCalls`, the
* wrapper for the receiver `structural` are the following instead:
* and eventually reaches the back-end as
*
* {{{
* reflectiveSelectableFromLangReflectiveCalls(structural)(
* using scala.languageFeature.reflectiveCalls)
* selectable.selectDynamic("foo") // same as above
* selectable.applyDynamic("bar",
* wrapRefArray([ classOf[Int], classOf[String] : jl.Class ]
* )(
* genericWrapArray([ Int.box(6), "hello" : Object ])
* )
* }}}
*
* (in which case we don't care about the contextual argument).
*
* In SJSIR, they must be encoded as follows:
*
* {{{
* structural.foo;R()
* structural.bar;I;Ljava.lang.String;R(
* selectable.selectedValue;O().foo;R()
* selectable.selectedValue;O().bar;I;Ljava.lang.String;R(
* Int.box(6).asInstanceOf[int],
* "hello".asInstanceOf[java.lang.String]
* )
* }}}
*
* where `selectedValue;O()` is declared in `scala.reflect.Selectable` and
* holds the actual instance on which to perform the reflective operations.
* For the typical use case from the first snippet, it returns `structural`.
*
* This means that we must deconstruct the elaborated calls to recover:
*
* - the original receiver `structural`
* - the method name as a compile-time string `foo` or `bar`
* - the `tp: Type`s that have been wrapped in `ClassTag(classOf[tp])`, as a
* - the `tp: Type`s that have been wrapped in `classOf[tp]`, as a
* compile-time List[Type], from which we'll derive `jstpe.Type`s for the
* `asInstanceOf`s and `jstpe.TypeRef`s for the `MethodName.reflectiveProxy`
* - the actual arguments as a compile-time `List[Tree]`
Expand All @@ -3609,26 +3615,10 @@ class JSCodeGen()(using genCtx: Context) {
*/
private def genReflectiveCall(tree: Apply, isSelectDynamic: Boolean): js.Tree = {
implicit val pos = tree.span
val Apply(fun @ Select(receiver0, _), args) = tree
val Apply(fun @ Select(receiver, _), args) = tree

/* Extract the real receiver, which is the first argument to one of the
* implicit conversions scala.reflect.Selectable.reflectiveSelectable or
* scala.Selectable.reflectiveSelectableFromLangReflectiveCalls.
*/
val receiver = receiver0 match {
case Apply(fun1, receiver :: _)
if fun1.symbol == jsdefn.ReflectSelectable_reflectiveSelectable ||
fun1.symbol == jsdefn.Selectable_reflectiveSelectableFromLangReflectiveCalls =>
genExpr(receiver)

case _ =>
report.error(
"The receiver of Selectable.selectDynamic or Selectable.applyDynamic " +
"must be a call to the (implicit) method scala.reflect.Selectable.reflectiveSelectable. " +
"Other uses are not supported in Scala.js.",
tree.sourcePos)
js.Undefined()
}
val selectedValueTree = js.Apply(js.ApplyFlags.empty, genExpr(receiver),
js.MethodIdent(selectedValueMethodName), Nil)(jstpe.AnyType)

// Extract the method name as a String
val methodNameStr = args.head match {
Expand All @@ -3648,38 +3638,30 @@ class JSCodeGen()(using genCtx: Context) {
} else {
// Extract the param type refs and actual args from the 2nd and 3rd argument to applyDynamic
args.tail match {
case WrapArray(classTagsArray: JavaSeqLiteral) :: WrapArray(actualArgsAnyArray: JavaSeqLiteral) :: Nil =>
// Extract jstpe.Type's and jstpe.TypeRef's from the ClassTag.apply(_) trees
val formalParamTypesAndTypeRefs = classTagsArray.elems.map {
// ClassTag.apply(classOf[tp]) -> tp
case Apply(fun, Literal(const) :: Nil)
if fun.symbol == defn.ClassTagModule_apply && const.tag == Constants.ClazzTag =>
case WrapArray(classOfsArray: JavaSeqLiteral) :: WrapArray(actualArgsAnyArray: JavaSeqLiteral) :: Nil =>
// Extract jstpe.Type's and jstpe.TypeRef's from the classOf[_] trees
val formalParamTypesAndTypeRefs = classOfsArray.elems.map {
// classOf[tp] -> tp
case Literal(const) if const.tag == Constants.ClazzTag =>
toIRTypeAndTypeRef(const.typeValue)
// ClassTag.SpecialType -> erasure(SepecialType.typeRef) (e.g., ClassTag.Any -> Object)
case Apply(Select(classTagModule, name), Nil)
if classTagModule.symbol == defn.ClassTagModule &&
defn.SpecialClassTagClasses.exists(_.name == name.toTypeName) =>
toIRTypeAndTypeRef(TypeErasure.erasure(
defn.SpecialClassTagClasses.find(_.name == name.toTypeName).get.typeRef))
// Anything else is invalid
case classTag =>
case otherTree =>
report.error(
"The ClassTags passed to Selectable.applyDynamic must be " +
"literal ClassTag(classOf[T]) expressions " +
"(typically compiler-generated). " +
"The java.lang.Class[_] arguments passed to Selectable.applyDynamic must be " +
"literal classOf[T] expressions (typically compiler-generated). " +
"Other uses are not supported in Scala.js.",
classTag.sourcePos)
otherTree.sourcePos)
(jstpe.AnyType, jstpe.ClassRef(jsNames.ObjectClass))
}

// Gen the actual args, downcasting them to the formal param types
val actualArgs = actualArgsAnyArray.elems.zip(formalParamTypesAndTypeRefs).map {
(actualArgAny, formalParamTypeAndTypeRef) =>
val genActualArgAny = genExpr(actualArgAny)
js.AsInstanceOf(genActualArgAny, formalParamTypeAndTypeRef._1)(genActualArgAny.pos)
genAsInstanceOf(genActualArgAny, formalParamTypeAndTypeRef._1)(genActualArgAny.pos)
}

(formalParamTypesAndTypeRefs.map(_._2), actualArgs)
(formalParamTypesAndTypeRefs.map(pair => toParamOrResultTypeRef(pair._2)), actualArgs)

case _ =>
report.error(
Expand All @@ -3692,7 +3674,7 @@ class JSCodeGen()(using genCtx: Context) {

val methodName = MethodName.reflectiveProxy(methodNameStr, formalParamTypeRefs)

js.Apply(js.ApplyFlags.empty, receiver, js.MethodIdent(methodName), actualArgs)(jstpe.AnyType)
js.Apply(js.ApplyFlags.empty, selectedValueTree, js.MethodIdent(methodName), actualArgs)(jstpe.AnyType)
}

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

private val ObjectClassRef = jstpe.ClassRef(ir.Names.ObjectClass)

private val newSimpleMethodName = SimpleMethodName("new")

private val ObjectArgConstructorName =
MethodName.constructor(List(jstpe.ClassRef(ir.Names.ObjectClass)))
private val selectedValueMethodName = MethodName("selectedValue", Nil, ObjectClassRef)

private val ObjectArgConstructorName = MethodName.constructor(List(ObjectClassRef))

private val thisOriginalName = OriginalName("this")

Expand Down
18 changes: 11 additions & 7 deletions compiler/src/dotty/tools/backend/sjs/JSEncoding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,8 @@ object JSEncoding {
}

/** Computes the type ref for a type, to be used in a method signature. */
private def paramOrResultTypeRef(tpe: Type)(using Context): jstpe.TypeRef = {
toTypeRef(tpe) match {
case jstpe.ClassRef(ScalaRuntimeNullClassName) => jstpe.NullRef
case jstpe.ClassRef(ScalaRuntimeNothingClassName) => jstpe.NothingRef
case otherTypeRef => otherTypeRef
}
}
private def paramOrResultTypeRef(tpe: Type)(using Context): jstpe.TypeRef =
toParamOrResultTypeRef(toTypeRef(tpe))

def encodeLocalSym(sym: Symbol)(
implicit ctx: Context, pos: ir.Position, localNames: LocalNameGenerator): js.LocalIdent = {
Expand Down Expand Up @@ -284,6 +279,15 @@ object JSEncoding {
ClassName(sym1.javaClassName)
}

/** Converts a general TypeRef to a TypeRef to be used in a method signature. */
def toParamOrResultTypeRef(typeRef: jstpe.TypeRef): jstpe.TypeRef = {
typeRef match {
case jstpe.ClassRef(ScalaRuntimeNullClassName) => jstpe.NullRef
case jstpe.ClassRef(ScalaRuntimeNothingClassName) => jstpe.NothingRef
case _ => typeRef
}
}

def toIRTypeAndTypeRef(tp: Type)(using Context): (jstpe.Type, jstpe.TypeRef) = {
val typeRefInternal = toTypeRefInternal(tp)
(toIRTypeInternal(typeRefInternal), typeRefInternal._1)
Expand Down
46 changes: 25 additions & 21 deletions compiler/src/dotty/tools/dotc/typer/Dynamic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dotty.tools.dotc.core.Names.{Name, TermName}
import dotty.tools.dotc.core.StdNames._
import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.core.Decorators._
import dotty.tools.dotc.core.TypeErasure
import util.Spans._
import core.Symbols._
import core.Definitions
Expand Down Expand Up @@ -149,22 +150,22 @@ trait Dynamic {
* x1.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
* .asInstanceOf[R]
* ```
* If this call resolves to an `applyDynamic` method that takes a `ClassTag[?]*` as second
* If this call resolves to an `applyDynamic` method that takes a `Class[?]*` as second
* parameter, we further rewrite this call to
* scala```
* x1.applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)
* x1.applyDynamic("a", c11, ..., c1n, ..., cN1, ... cNn)
* (a11, ..., a1n, ..., aN1, ..., aNn)
* .asInstanceOf[R]
* ```
* where CT11, ..., CTNn are the class tags representing the erasure of T11, ..., TNn.
* where c11, ..., cNn are the classOf constants representing the erasures of T11, ..., TNn.
*
* It's an error if U is neither a value nor a method type, or a dependent method
* type.
*/
def handleStructural(tree: Tree)(using Context): Tree = {
val (fun @ Select(qual, name), targs, vargss) = decomposeCall(tree)

def structuralCall(selectorName: TermName, ctags: => List[Tree]) = {
def structuralCall(selectorName: TermName, classOfs: => List[Tree]) = {
val selectable = adapt(qual, defn.SelectableClass.typeRef)

// ($qual: Selectable).$selectorName("$name")
Expand All @@ -177,27 +178,27 @@ trait Dynamic {
if (vargss.isEmpty) base
else untpd.Apply(base, vargss.flatten.map(untpd.TypedSplice(_)))

// If function is an `applyDynamic` that takes a ClassTag* parameter,
// add `ctags`.
def addClassTags(tree: Tree): Tree = tree match
// If function is an `applyDynamic` that takes a Class* parameter,
// add `classOfs`.
def addClassOfs(tree: Tree): Tree = tree match
case Apply(fn: Apply, args) =>
cpy.Apply(tree)(addClassTags(fn), args)
cpy.Apply(tree)(addClassOfs(fn), args)
case Apply(fn @ Select(_, nme.applyDynamic), nameArg :: _ :: Nil) =>
fn.tpe.widen match
case mt: MethodType => mt.paramInfos match
case _ :: ctagsParam :: Nil
if ctagsParam.isRepeatedParam
&& ctagsParam.argInfos.head.isRef(defn.ClassTagClass) =>
val ctagType = defn.ClassTagClass.typeRef.appliedTo(TypeBounds.empty)
case _ :: classOfsParam :: Nil
if classOfsParam.isRepeatedParam
&& classOfsParam.argInfos.head.isRef(defn.ClassClass) =>
val jlClassType = defn.ClassClass.typeRef.appliedTo(TypeBounds.empty)
cpy.Apply(tree)(fn,
nameArg :: seqToRepeated(SeqLiteral(ctags, TypeTree(ctagType))) :: Nil)
nameArg :: seqToRepeated(SeqLiteral(classOfs, TypeTree(jlClassType))) :: Nil)
case _ => tree
case other => tree
case _ => tree
addClassTags(typed(scall))
addClassOfs(typed(scall))
}

def fail(name: Name, reason: String) =
def fail(reason: String): Tree =
errorTree(tree, em"Structural access not allowed on method $name because it $reason")

fun.tpe.widen match {
Expand All @@ -215,18 +216,21 @@ trait Dynamic {
}

if (isDependentMethod(tpe))
fail(name, i"has a method type with inter-parameter dependencies")
fail(i"has a method type with inter-parameter dependencies")
else {
def ctags = tpe.paramInfoss.flatten.map(pt =>
implicitArgTree(defn.ClassTagClass.typeRef.appliedTo(pt.widenDealias :: Nil), fun.span.endPos))
structuralCall(nme.applyDynamic, ctags).cast(tpe.finalResultType)
def classOfs =
if tpe.paramInfoss.nestedExists(!TypeErasure.hasStableErasure(_)) then
fail(i"has a parameter type with an unstable erasure") :: Nil
else
TypeErasure.erasure(tpe).asInstanceOf[MethodType].paramInfos.map(clsOf(_))
structuralCall(nme.applyDynamic, classOfs).cast(tpe.finalResultType)
}

// (@allanrenucci) I think everything below is dead code
case _: PolyType =>
fail(name, "is polymorphic")
fail("is polymorphic")
case tpe =>
fail(name, i"has an unsupported type: $tpe")
fail(i"has an unsupported type: $tpe")
}
}
}
16 changes: 10 additions & 6 deletions docs/docs/reference/changed-features/structural-types-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ def selectDynamic(name: String): T
```
Often, the return type `T` is `Any`.

The `applyDynamic` method is used for selections that are applied to arguments. It takes a method name and possibly `ClassTag`s representing its parameters types as well as the arguments to pass to the function.
Unlike `scala.Dynamic`, there is no special meaning for an `updateDynamic` method.
However, we reserve the right to give it meaning in the future.
Consequently, it is recommended not to define any member called `updateDynamic` in `Selectable`s.

The `applyDynamic` method is used for selections that are applied to arguments. It takes a method name and possibly `Class`es representing its parameters types as well as the arguments to pass to the function.
Its signature should be of one of the two following forms:
```scala
def applyDynamic(name: String)(args: Any*): T
def applyDynamic(name: String, ctags: ClassTag[?]*)(args: Any*): T
def applyDynamic(name: String, ctags: Class[?]*)(args: Any*): T
```
Both versions are passed the actual arguments in the `args` parameter. The second version takes in addition a vararg argument of class tags that identify the method's parameter classes. Such an argument is needed
Both versions are passed the actual arguments in the `args` parameter. The second version takes in addition a vararg argument of `java.lang.Class`es that identify the method's parameter classes. Such an argument is needed
if `applyDynamic` is implemented using Java reflection, but it could be
useful in other cases as well. `selectDynamic` and `applyDynamic` can also take additional context parameters in using clauses. These are resolved in the normal way at the callsite.

Expand All @@ -58,13 +62,13 @@ and `Rs` are structural refinement declarations, and given `v.a` of type `U`, we
v.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
.asInstanceOf[R]
```
If this call resolves to an `applyDynamic` method of the second form that takes a `ClassTag[?]*` argument, we further rewrite this call to
If this call resolves to an `applyDynamic` method of the second form that takes a `Class[?]*` argument, we further rewrite this call to
```scala
v.applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)(
v.applyDynamic("a", c11, ..., c1n, ..., cN1, ... cNn)(
a11, ..., a1n, ..., aN1, ..., aNn)
.asInstanceOf[R]
```
where each `CT_ij` is the class tag of the type of the formal parameter `Tij`
where each `c_ij` is the literal `java.lang.Class[?]` of the type of the formal parameter `Tij`, i.e., `classOf[Tij]`.

- If `U` is neither a value nor a method type, or a dependent method
type, an error is emitted.
Expand Down
Loading