Skip to content

Commit 1a6224b

Browse files
committed
Preserve correct behaviour of structural selection for reflective and non-reflective access
1 parent 4b8a31f commit 1a6224b

File tree

4 files changed

+84
-25
lines changed

4 files changed

+84
-25
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ class Definitions {
808808
@tu lazy val ClassTagModule: Symbol = ClassTagClass.companionModule
809809
@tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply)
810810

811+
@tu lazy val ReflectSelectableTypeRef: TypeRef = requiredClassRef("scala.reflect.Selectable")
812+
811813
@tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest")
812814
@tu lazy val TypeTest_unapply: Symbol = TypeTestClass.requiredMethod(nme.unapply)
813815
@tu lazy val TypeTestModule_identity: Symbol = TypeTestClass.companionModule.requiredMethod(nme.identity)

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

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,33 @@ trait Dynamic {
216216
def fail(reason: String): Tree =
217217
errorTree(tree, em"Structural access not allowed on method $name because it $reason")
218218

219-
fun.tpe.widen match {
220-
case tpe: ValueType if ValueClasses.isDerivedValueClass(tpe.classSymbol) =>
221-
val underlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass).info.resultType.asSeenFrom(tpe, tpe.classSymbol)
222-
val wrapped = structuralCall(nme.selectDynamic, Nil)
223-
val resultTree = if wrapped.tpe.isExactlyAny then New(tpe, wrapped.cast(underlying) :: Nil) else wrapped
224-
resultTree.cast(tpe)
219+
extension (tree: Tree)
220+
/** The implementations of `selectDynamic` and `applyDynamic` in `scala.reflect.SelectDynamic` have no information about the expected return type of a value/method which was declared in the refinement,
221+
* only the JVM type after erasure can be obtained through reflection, e.g.
222+
*
223+
* class Foo(val i: Int) extends AnyVal
224+
* class Reflective extends reflect.Selectable
225+
* val reflective = new Reflective {
226+
* def foo = Foo(1) // Foo at compile time, java.lang.Integer in reflection
227+
* }
228+
*
229+
* Because of that reflective access cannot be implemented properly in `scala.reflect.SelectDynamic` itself
230+
* because it's not known there if the value should be wrapped in a value class constructor call or not.
231+
* Hence the logic of wrapping is performed here, relying on the fact that the implementations of `selectDynamic` and `applyDynamic` in `scala.reflect.SelectDynamic` are final.
232+
*/
233+
def maybeBoxingCast(tpe: Type) =
234+
val maybeBoxed =
235+
if ValueClasses.isDerivedValueClass(tpe.classSymbol) && qual.tpe <:< defn.ReflectSelectableTypeRef then
236+
val genericUnderlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass)
237+
val underlying = tpe.select(genericUnderlying).widen.resultType
238+
New(tpe, tree.cast(underlying) :: Nil)
239+
else
240+
tree
241+
maybeBoxed.cast(tpe)
225242

243+
fun.tpe.widen match {
226244
case tpe: ValueType =>
227-
structuralCall(nme.selectDynamic, Nil).cast(tpe)
245+
structuralCall(nme.selectDynamic, Nil).maybeBoxingCast(tpe)
228246

229247
case tpe: MethodType =>
230248
def isDependentMethod(tpe: Type): Boolean = tpe match {
@@ -244,11 +262,7 @@ trait Dynamic {
244262
fail(i"has a parameter type with an unstable erasure") :: Nil
245263
else
246264
TypeErasure.erasure(tpe).asInstanceOf[MethodType].paramInfos.map(clsOf(_))
247-
val finalTpe = tpe.finalResultType
248-
if ValueClasses.isDerivedValueClass(finalTpe.classSymbol) then
249-
New(finalTpe, structuralCall(nme.applyDynamic, classOfs).cast(finalTpe)
250-
.cast(ValueClasses.valueClassUnbox(finalTpe.classSymbol.asClass).info.resultType.asSeenFrom(finalTpe, finalTpe.classSymbol)) :: Nil)
251-
else structuralCall(nme.applyDynamic, classOfs).cast(finalTpe)
265+
structuralCall(nme.applyDynamic, classOfs).maybeBoxingCast(tpe.finalResultType)
252266
}
253267

254268
// (@allanrenucci) I think everything below is dead code

tests/run/i14340.check

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
1
2+
1
3+
2
24
2
3-
3
4-
qux
5+
10
6+
10
7+
20
8+
20
9+
100
10+
100

tests/run/i14340.scala

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,57 @@
1+
class Container1 extends reflect.Selectable
2+
3+
class Container2(values: Map[String, Any], methods: Map[String, Int => Any]) extends Selectable:
4+
def selectDynamic(name: String) = values(name)
5+
def applyDynamic(name: String)(arg: Int) = methods(name)(arg)
6+
17
class Foo(val value: Int) extends AnyVal
28
class Bar[A](val value: A) extends AnyVal
39

4-
class Container1 extends reflect.Selectable
10+
object Helpers:
11+
def foo = Foo(1)
12+
def bar = Bar(Foo(2))
13+
def qux1 = Bar(new Container1 { def foo = Foo(10) })
14+
def qux2 = Bar(new Container2(Map("foo" -> Foo(20)), Map.empty).asInstanceOf[Container2 { def foo: Foo }])
15+
16+
@main def Test: Unit =
17+
val cont1 = new Container1:
18+
def foo = Helpers.foo
19+
val bar = Helpers.bar
20+
def qux1 = Helpers.qux1
21+
def qux2 = Helpers.qux2
22+
def fooFromInt(i: Int) = Foo(i)
523

6-
class Container2 extends Selectable:
7-
def selectDynamic(name: String) = Bar(name)
24+
val cont2values = Map(
25+
"foo" -> Helpers.foo,
26+
"bar" -> Helpers.bar,
27+
"qux1" -> Helpers.qux1,
28+
"qux2" -> Helpers.qux2
29+
)
830

9-
val cont1 = new Container1:
10-
def foo = new Foo(1)
11-
val bar = new Bar(Foo(2))
12-
def fooFromInt(i: Int) = new Foo(i)
31+
val cont2methods = Map(
32+
"fooFromInt" -> { (i: Int) => Foo(i) }
33+
)
1334

14-
val cont2 = (new Container2).asInstanceOf[Container2 { def qux: Bar[String] }]
35+
val cont2 = Container2(cont2values, cont2methods).asInstanceOf[Container2 {
36+
def foo: Foo
37+
def bar: Bar[Foo]
38+
def qux1: Bar[Container1 { def foo: Foo }]
39+
def qux2: Bar[Container2 { def foo: Foo }]
40+
def fooFromInt(i: Int): Foo
41+
}]
42+
1543

16-
@main def Test: Unit =
1744
println(cont1.foo.value)
45+
println(cont2.foo.value)
46+
1847
println(cont1.bar.value.value)
19-
println(cont1.fooFromInt(3).value)
20-
println(cont2.qux.value)
48+
println(cont2.bar.value.value)
49+
50+
println(cont1.qux1.value.foo.value)
51+
println(cont2.qux1.value.foo.value)
52+
53+
println(cont1.qux2.value.foo.value)
54+
println(cont2.qux2.value.foo.value)
55+
56+
println(cont1.fooFromInt(100).value)
57+
println(cont2.fooFromInt(100).value)

0 commit comments

Comments
 (0)