diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 71b85d97a187..ab74a4cd099b 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1592,6 +1592,11 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def repeated(trees: List[Tree], tpt: Tree)(using Context): Tree = ctx.typeAssigner.arrayToRepeated(JavaSeqLiteral(trees, tpt)) + /** Convert a list of trees to a vararg-compatible tree represented as SeqLiteral. + */ + def repeatedSeq(trees: List[Tree], tpt: Tree)(using Context): Tree = + ctx.typeAssigner.seqToRepeated(SeqLiteral(trees, tpt)) + /** Create a tree representing a list containing all * the elements of the argument list. A "list of tree to * tree of list" conversion. diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala index 14cc7bf963a6..fc76e70178cc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala +++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala @@ -3,8 +3,7 @@ package dotc package typer import dotty.tools.dotc.ast.Trees.* -import dotty.tools.dotc.ast.tpd -import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.{tpd, untpd} import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Flags.* @@ -149,6 +148,9 @@ trait Dynamic { untpd.Apply(selectWithTypes, Literal(Constant(name.toString))) } + extension (tpe: Type) + def isReflectSelectableTypeRef(using Context): Boolean = tpe <:< defn.ReflectSelectableTypeRef + /** Handle reflection-based dispatch for members of structural types. * * Given `x.a`, where `x` is of (widened) type `T` (a value type or a nullary method type), @@ -178,10 +180,27 @@ trait Dynamic { * 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 = { + def handleStructural(tree: Tree)(using Context) = { val fun @ Select(qual, name) = funPart(tree): @unchecked val vargss = termArgss(tree) + def handleRepeated(base: untpd.Tree, possiblyCurried: List[List[Tree]], isReflectSelectable: Boolean) = { + val args = possiblyCurried.flatten.map { t => + val clzSym = t.tpe.resultType.classSymbol.asClass + if clzSym.isDerivedValueClass && isReflectSelectable then + val underlying = ValueClasses.valueClassUnbox(clzSym).asTerm + tpd.Select(t, underlying.name) + else + t + } + val handledFlat = + if isReflectSelectable then List(untpd.TypedSplice(tpd.repeatedSeq(args, TypeTree(defn.AnyType)))) + else if args.isEmpty then List() + else List(untpd.TypedSplice(tpd.repeatedSeq(args, TypeTree(args.map(_.tpe).reduce(_ | _))))) + + untpd.Apply(base, handledFlat) + } + def structuralCall(selectorName: TermName, classOfs: => List[Tree]) = { val selectable = adapt(qual, defn.SelectableClass.typeRef | defn.DynamicClass.typeRef) @@ -193,7 +212,11 @@ trait Dynamic { val scall = if (vargss.isEmpty) base - else untpd.Apply(base, vargss.flatten.map(untpd.TypedSplice(_))) + else handleRepeated( + base, + vargss, + qual.tpe.isReflectSelectableTypeRef || selectable.tpe.isReflectSelectableTypeRef + ) // If function is an `applyDynamic` that takes a Class* parameter, // add `classOfs`. @@ -239,7 +262,7 @@ trait Dynamic { */ def maybeBoxingCast(tpe: Type) = val maybeBoxed = - if tpe.classSymbol.isDerivedValueClass && qual.tpe <:< defn.ReflectSelectableTypeRef then + if tpe.classSymbol.isDerivedValueClass && qual.tpe.isReflectSelectableTypeRef then val genericUnderlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass) val underlying = tpe.select(genericUnderlying).widen.resultType New(tpe.widen, tree.cast(underlying) :: Nil) @@ -268,7 +291,12 @@ trait Dynamic { 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(_)) + TypeErasure.erasure(tpe).asInstanceOf[MethodType].paramInfos.map { tpe => + if tpe.widen.classSymbol.isDerivedValueClass then + clsOf(ValueClasses.valueClassUnbox(tpe.classSymbol.asClass).info) + else + clsOf(tpe) + } structuralCall(nme.applyDynamic, classOfs).maybeBoxingCast(tpe.finalResultType) } diff --git a/docs/_docs/reference/changed-features/structural-types-spec.md b/docs/_docs/reference/changed-features/structural-types-spec.md index 216b738ae61c..be33201d53b2 100644 --- a/docs/_docs/reference/changed-features/structural-types-spec.md +++ b/docs/_docs/reference/changed-features/structural-types-spec.md @@ -55,6 +55,18 @@ Both versions are passed the actual arguments in the `args` parameter. The secon 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. +Typically, vararg arguments of a function at callsite are represented as an `Array(elems)`. But in case of `scala.Selectable` implementation relied on Java reflection `scala.reflect.Selectable` when vararg arguments passed to `applyDynamic` method, they get interpreted as single parameter instead of multiple. +To overcome this they should be wrapped in `Seq(elems)` to be considered as multiple parameters at callsite of `scala.reflect.Selectable.applyDynamic`. +```scala +class Reflective extends reflect.Selectable +class Foo(val i: Int) extends AnyVal +val reflective = new Reflective { + def varargs(x: Int, foo: Foo*) = foo.map(_.i).sum + x +} +val varargs = List(Foo(1), Foo(2), Foo(3)) +reflective.varargs(1, varargs:_*) +``` + Given a value `v` of type `C { Rs }`, where `C` is a class reference and `Rs` are structural refinement declarations, and given `v.a` of type `U`, we consider three distinct cases: diff --git a/tests/pos/i17100.scala b/tests/pos/i17100.scala index 1858e0383f8d..5d63f6d1a794 100644 --- a/tests/pos/i17100.scala +++ b/tests/pos/i17100.scala @@ -2,7 +2,7 @@ trait Sel extends Selectable extension (s: Sel) def selectDynamic(name: String) = ??? - def applyDynamic(name: String)(x: Int) = ??? + def applyDynamic(name: String)(x: Int*) = ??? def applyDynamic(name: String)() = ??? val sel = (new Sel {}).asInstanceOf[Sel{ def foo: String; def bar(x: Int): Int; def baz(): Int }] diff --git a/tests/run/i14340.scala b/tests/run/i14340.scala index 0670c7e471ac..230e08d53428 100644 --- a/tests/run/i14340.scala +++ b/tests/run/i14340.scala @@ -1,8 +1,8 @@ class Container1 extends reflect.Selectable -class Container2(values: Map[String, Any], methods: Map[String, Int => Any]) extends Selectable: +class Container2(values: Map[String, Any], methods: Map[String, Seq[Int] => Any]) extends Selectable: def selectDynamic(name: String) = values(name) - def applyDynamic(name: String)(arg: Int) = methods(name)(arg) + def applyDynamic(name: String)(arg: Int*) = methods(name)(arg) class Foo(val value: Int) extends AnyVal class Bar[A](val value: A) extends AnyVal @@ -29,7 +29,7 @@ object Helpers: ) val cont2methods = Map( - "fooFromInt" -> { (i: Int) => Foo(i) } + "fooFromInt" -> { (i: Seq[Int]) => Foo(i.head) } ) val cont2 = Container2(cont2values, cont2methods).asInstanceOf[Container2 { @@ -39,7 +39,7 @@ object Helpers: def qux2: Bar[Container2 { def foo: Foo }] def fooFromInt(i: Int): Foo }] - + println(cont1.foo.value) println(cont2.foo.value) @@ -49,7 +49,7 @@ object Helpers: println(cont1.qux1.value.foo.value) println(cont2.qux1.value.foo.value) - + println(cont1.qux2.value.foo.value) println(cont2.qux2.value.foo.value) diff --git a/tests/run/i16995.check b/tests/run/i16995.check new file mode 100644 index 000000000000..45aa0643ce08 --- /dev/null +++ b/tests/run/i16995.check @@ -0,0 +1,62 @@ +foo +foo0 +fooI 1 +fooN 1 +fooII 1 2 +fooNN 1 2 +fooIvarargs 1 2 +fooNvarargs 1 2 +fooIseq 1 2 +fooNseq 1 2 +fooIIvarargs 1 2 +fooNNvarargs 1 2 +fooI_I 1 2 +fooN_N 1 2 +foo0_II 1 2 +foo0_NN 1 2 +foo0_Ivarargs 1 2 +foo0_Nvarargs 1 2 +foo0_I_I 1 2 +foo0_N_N 1 2 + +foo +foo0 +fooI 1 +fooN 1 +fooII 1 2 +fooNN 1 2 +fooIvarargs 1 2 +fooNvarargs 1 2 +fooIseq 1 2 +fooNseq 1 2 +fooIIvarargs 1 2 3 +fooNNvarargs 1 2 +fooI_I 1 2 +fooN_N 1 2 +foo0_II 1 2 +foo0_NN 1 2 +foo0_Ivarargs 1 2 +foo0_Nvarargs 1 2 +foo0_I_I 1 2 +foo0_N_N 1 2 + +foo +foo0 +fooI 1 +fooN 1 +fooII 1 2 +fooNN 1 2 +fooIvarargs 1 2 +fooNvarargs 1 2 +fooIseq 1 2 +fooNseq 1 2 +fooIIvarargs 1 2 +fooNNvarargs 1 2 +fooI_I 1 2 +fooN_N 1 2 +foo0_II 1 2 +foo0_NN 1 2 +foo0_Ivarargs 1 2 +foo0_Nvarargs 1 2 +foo0_I_I 1 2 +foo0_N_N 1 2 diff --git a/tests/run/i16995.scala b/tests/run/i16995.scala new file mode 100644 index 000000000000..d23e9b0f0d8d --- /dev/null +++ b/tests/run/i16995.scala @@ -0,0 +1,154 @@ +import scala.language.dynamics +import scala.reflect.Selectable.reflectiveSelectable + +object Test { + class Num(val i: Int) extends AnyVal + + def show(x: Int | Num | Seq[Int | Num]): String = x match + case i: Int => i.toString + case num: Num => num.i.toString + case seq: Seq[Int | Num] => seq.map(show).mkString(" ") + + trait Nonreflective extends Selectable: + def selectDynamic(name: String): String = name + def applyDynamic(name: String)(args: (Int | Num | Seq[Int | Num])*): String = + val argsString = args.map(show).mkString(" ", " ", "") + s"${name}${argsString}" + + trait Dynamic0 extends Dynamic: + def selectDynamic(name: String): String = name + + trait Dynamic1 extends Dynamic: + def applyDynamic(name: String)(args1: (Int | Num | Seq[Int | Num])*): String = + val argsString = args1.map(show).mkString(" ", " ", "") + s"${name}${argsString}" + + trait Dynamic2 extends Dynamic: + def applyDynamic(name: String)(args1: (Int | Num)*)(args2: (Int | Num)*): String = + val argsString = (args1 ++ args2).map(show).mkString(" ", " ", "") + s"${name}${argsString}" + + trait Dynamic3 extends Dynamic: + def applyDynamic(name: String)(args1: (Int | Num)*)(args2: (Int | Num)*)(args3: (Int | Num)*): String = + val argsString = (args1 ++ args2 ++ args3).map(show).mkString(" ", " ", "") + s"${name}${argsString}" + + type Api = { + def foo: String + def foo0(): String + def fooI(i: Int): String + def fooN(n: Num): String + def fooII(i1: Int, i2: Int): String + def fooNN(n1: Num, n2: Num): String + def fooIvarargs(is: Int*): String + def fooNvarargs(ns: Num*): String + def fooIseq(is: Seq[Int]): String + def fooNseq(ns: Seq[Num]): String + def fooIIvarargs(i1: Int, is: Int*): String + def fooNNvarargs(n1: Num, ns: Num*): String + def fooI_I(i1: Int)(i2: Int): String + def fooN_N(n1: Num)(n2: Num): String + def foo0_II()(i1: Int, i2: Int): String + def foo0_NN()(n1: Num, n2: Num): String + def foo0_Ivarargs()(is: Int*): String + def foo0_Nvarargs()(ns: Num*): String + def foo0_I_I()(i1: Int)(i2: Int): String + def foo0_N_N()(n1: Num)(n2: Num): String + } + + class ClassImpl { + def foo: String = "foo" + def foo0(): String = "foo0" + def fooI(i: Int): String = s"fooI ${i}" + def fooN(n: Num): String = s"fooN ${n.i}" + def fooII(i1: Int, i2: Int): String = s"fooII ${i1} ${i2}" + def fooNN(n1: Num, n2: Num): String = s"fooNN ${n1.i} ${n2.i}" + def fooIvarargs(is: Int*): String = s"fooIvarargs${is.mkString(" ", " ", "")}" + def fooNvarargs(ns: Num*): String = s"fooNvarargs${ns.map(_.i).mkString(" ", " ", "")}" + def fooIseq(is: Seq[Int]): String = s"fooIseq${is.mkString(" ", " ", "")}" + def fooNseq(ns: Seq[Num]): String = s"fooNseq${ns.map(_.i).mkString(" ", " ", "")}" + def fooIIvarargs(i1: Int, is: Int*): String = s"fooIIvarargs ${i1}${is.mkString(" ", " ", "")}" + def fooNNvarargs(n1: Num, ns: Num*): String = s"fooNNvarargs ${n1.i}${ns.map(_.i).mkString(" ", " ", "")}" + def fooI_I(i1: Int)(i2: Int): String = s"fooI_I ${i1} ${i2}" + def fooN_N(n1: Num)(n2: Num): String = s"fooN_N ${n1.i} ${n2.i}" + def foo0_II()(i1: Int, i2: Int): String = s"foo0_II ${i1} ${i2}" + def foo0_NN()(n1: Num, n2: Num): String = s"foo0_NN ${n1.i} ${n2.i}" + def foo0_Ivarargs()(is: Int*): String = s"foo0_Ivarargs${is.mkString(" ", " ", "")}" + def foo0_Nvarargs()(ns: Num*): String = s"foo0_Nvarargs${ns.map(_.i).mkString(" ", " ", "")}" + def foo0_I_I()(i1: Int)(i2: Int): String = s"foo0_I_I ${i1} ${i2}" + def foo0_N_N()(n1: Num)(n2: Num): String = s"foo0_N_N ${n1.i} ${n2.i}" + } + + + def main(args: Array[String]): Unit = { + val reflective: Api = new ClassImpl() + val nonreflective: Nonreflective & Api = (new Nonreflective {}).asInstanceOf[Nonreflective & Api] + val dynamic0 = new Dynamic0 {} + val dynamic1 = new Dynamic1 {} + val dynamic2 = new Dynamic2 {} + val dynamic3 = new Dynamic3 {} + + println(reflective.foo) + println(reflective.foo0()) + println(reflective.fooI(1)) + println(reflective.fooN(new Num(1))) + println(reflective.fooII(1, 2)) + println(reflective.fooNN(new Num(1), new Num(2))) + println(reflective.fooIvarargs(1, 2)) + println(reflective.fooNvarargs(new Num(1), new Num(2))) + println(reflective.fooIseq(Seq(1, 2))) + println(reflective.fooNseq(Seq(new Num(1), new Num(2)))) + println(reflective.fooIIvarargs(1, 2)) + println(reflective.fooNNvarargs(new Num(1), new Num(2))) + println(reflective.fooI_I(1)(2)) + println(reflective.fooN_N(new Num(1))(new Num(2))) + println(reflective.foo0_II()(1, 2)) + println(reflective.foo0_NN()(new Num(1), new Num(2))) + println(reflective.foo0_Ivarargs()(1, 2)) + println(reflective.foo0_Nvarargs()(new Num(1), new Num(2))) + println(reflective.foo0_I_I()(1)(2)) + println(reflective.foo0_N_N()(new Num(1))(new Num(2))) + println() + println(nonreflective.foo) + println(nonreflective.foo0()) + println(nonreflective.fooI(1)) + println(nonreflective.fooN(new Num(1))) + println(nonreflective.fooII(1, 2)) + println(nonreflective.fooNN(new Num(1), new Num(2))) + println(nonreflective.fooIvarargs(1, 2)) + println(nonreflective.fooNvarargs(new Num(1), new Num(2))) + println(nonreflective.fooIseq(Seq(1, 2))) + println(nonreflective.fooNseq(Seq(new Num(1), new Num(2)))) + println(nonreflective.fooIIvarargs(1, 2, 3)) + println(nonreflective.fooNNvarargs(new Num(1), new Num(2))) + println(nonreflective.fooI_I(1)(2)) + println(nonreflective.fooN_N(new Num(1))(new Num(2))) + println(nonreflective.foo0_II()(1, 2)) + println(nonreflective.foo0_NN()(new Num(1), new Num(2))) + println(nonreflective.foo0_Ivarargs()(1, 2)) + println(nonreflective.foo0_Nvarargs()(new Num(1), new Num(2))) + println(nonreflective.foo0_I_I()(1)(2)) + println(nonreflective.foo0_N_N()(new Num(1))(new Num(2))) + println() + println(dynamic0.foo) + println(dynamic1.foo0()) + println(dynamic1.fooI(1)) + println(dynamic1.fooN(new Num(1))) + println(dynamic1.fooII(1, 2)) + println(dynamic1.fooNN(new Num(1), new Num(2))) + println(dynamic1.fooIvarargs(1, 2)) + println(dynamic1.fooNvarargs(new Num(1), new Num(2))) + println(dynamic1.fooIseq(Seq(1, 2))) + println(dynamic1.fooNseq(Seq(new Num(1), new Num(2)))) + println(dynamic1.fooIIvarargs(1, 2)) + println(dynamic1.fooNNvarargs(new Num(1), new Num(2))) + println(dynamic2.fooI_I(1)(2)) + println(dynamic2.fooN_N(new Num(1))(new Num(2))) + println(dynamic2.foo0_II()(1, 2)) + println(dynamic2.foo0_NN()(new Num(1), new Num(2))) + println(dynamic2.foo0_Ivarargs()(1, 2)) + println(dynamic2.foo0_Nvarargs()(new Num(1), new Num(2))) + println(dynamic3.foo0_I_I()(1)(2)) + println(dynamic3.foo0_N_N()(new Num(1))(new Num(2))) + } +} \ No newline at end of file diff --git a/tests/sjs-junit/test/org/scalajs/testsuite/compiler/CustomReflectSelectableTestScala3.scala b/tests/sjs-junit/test/org/scalajs/testsuite/compiler/CustomReflectSelectableTestScala3.scala index 744a59bebc9d..a6517088bc82 100644 --- a/tests/sjs-junit/test/org/scalajs/testsuite/compiler/CustomReflectSelectableTestScala3.scala +++ b/tests/sjs-junit/test/org/scalajs/testsuite/compiler/CustomReflectSelectableTestScala3.scala @@ -15,12 +15,72 @@ class CustomReflectSelectableTestScala3 { val obj: reflect.Selectable { def foo(x: Int, y: String): String } = new CustomReflectSelectable(42) assertEquals("3 bar 42", obj.foo(3, "bar")) } + + @Test def callMethodWithValueClass(): Unit = { + val obj: reflect.Selectable { def bar(bar: Bar): Int } = new CustomReflectSelectable(42) + assertEquals(1, obj.bar(Bar(1))) + } + + @Test def callMethodWithVarargs(): Unit = { + val obj: reflect.Selectable { def varargs(x: Int, args: Bar*): Int } = new CustomReflectSelectable(42) + assertEquals(4, obj.varargs(2, Bar(1), Bar(1))) + } + + @Test def callMethodWithVarargsExpansion(): Unit = { + val args = Seq(Bar(1), Bar(1)) + val obj: reflect.Selectable {def varargs(x: Int, args: Bar*): Int} = new CustomReflectSelectable(42) + assertEquals(4, obj.varargs(2, args:_*)) + } + + @Test def callSelectableWithVarargs(): Unit = { + val cont2values = Map.empty[String, Any] + val cont2methods = Map[String, Seq[(Int | Bar | Seq[Bar])] => Int]( + "varargs" -> { (args: Seq[(Int | Bar | Seq[Bar])]) => args.map { + case x: Int => x + case b: Bar => b.x + case bs: Seq[Bar] => bs.map(_.x).sum + } .sum } + ) + val cont = ScalaSelectable(cont2values, cont2methods).asInstanceOf[ScalaSelectable { + def varargs(i: Int, foos: Bar*): Int + }] + assertEquals(3, cont.varargs(1, Bar(1), Bar(1))) + } + + @Test def callSelectableWithVarargsExpansion(): Unit = { + val cont2values = Map.empty[String, Any] + val cont2methods = Map[String, Seq[(Int | Bar | Seq[Bar])] => Int]( + "varargs" -> { + (args: Seq[(Int | Bar | Seq[Bar])]) => args.map { + case x: Int => x + case b: Bar => b.x + case bs: Seq[Bar] => bs.map(_.x).sum + }.sum + } + ) + val cont = ScalaSelectable(cont2values, cont2methods).asInstanceOf[ScalaSelectable { + def varargs(i: Int, foos: Bar*): Int + }] + val args = Seq(Bar(1), Bar(1)) + assertEquals(3, cont.varargs(1, args:_*)) + } } object CustomReflectSelectableTestScala3 { + class Bar(val x: Int) extends AnyVal class CustomReflectSelectable(param: Int) extends reflect.Selectable { val x: Int = 5 + param def foo(x: Int, y: String): String = s"$x $y $param" + + def bar(bar: Bar) = bar.x + + def varargs(x: Int, args: Bar*) = args.map(_.x).sum + x + } + + class ScalaSelectable(values: Map[String, Any], methods: Map[String, Seq[(Int | Bar | Seq[Bar])] => Int]) extends Selectable { + def selectDynamic(name: String): Any = values(name) + + def applyDynamic(name: String)(args: (Int | Bar | Seq[Bar])*): Int = methods(name)(args) } }