From 4b8a31fcc092b912caa6fc2a4ee55ebc4a4ac2d6 Mon Sep 17 00:00:00 2001 From: "s.bazarsadaev" Date: Sun, 6 Nov 2022 20:49:37 +0300 Subject: [PATCH 1/2] Closes #14340 - add dynamic access to value classes field --- .../src/dotty/tools/dotc/typer/Dynamic.scala | 32 +++++++++++++------ tests/run/i14340.check | 4 +++ tests/run/i14340.scala | 20 ++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 tests/run/i14340.check create mode 100644 tests/run/i14340.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala index 1630ce31e4c6..5de9ca6ab341 100644 --- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala +++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala @@ -2,20 +2,22 @@ package dotty.tools package dotc package typer -import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.ast.Trees.* import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.core.Constants.Constant -import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Contexts.* 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.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 ErrorReporting._ -import reporting._ +import util.Spans.* +import core.Symbols.* +import ErrorReporting.* +import dotty.tools.dotc.transform.ValueClasses +import dotty.tools.dotc.transform.TypeUtils.isPrimitiveValueType +import reporting.* object Dynamic { private def isDynamicMethod(name: Name): Boolean = @@ -215,6 +217,12 @@ trait Dynamic { errorTree(tree, em"Structural access not allowed on method $name because it $reason") fun.tpe.widen match { + case tpe: ValueType if ValueClasses.isDerivedValueClass(tpe.classSymbol) => + val underlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass).info.resultType.asSeenFrom(tpe, tpe.classSymbol) + val wrapped = structuralCall(nme.selectDynamic, Nil) + val resultTree = if wrapped.tpe.isExactlyAny then New(tpe, wrapped.cast(underlying) :: Nil) else wrapped + resultTree.cast(tpe) + case tpe: ValueType => structuralCall(nme.selectDynamic, Nil).cast(tpe) @@ -236,7 +244,11 @@ trait Dynamic { 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) + val finalTpe = tpe.finalResultType + if ValueClasses.isDerivedValueClass(finalTpe.classSymbol) then + New(finalTpe, structuralCall(nme.applyDynamic, classOfs).cast(finalTpe) + .cast(ValueClasses.valueClassUnbox(finalTpe.classSymbol.asClass).info.resultType.asSeenFrom(finalTpe, finalTpe.classSymbol)) :: Nil) + else structuralCall(nme.applyDynamic, classOfs).cast(finalTpe) } // (@allanrenucci) I think everything below is dead code diff --git a/tests/run/i14340.check b/tests/run/i14340.check new file mode 100644 index 000000000000..db372c9502e9 --- /dev/null +++ b/tests/run/i14340.check @@ -0,0 +1,4 @@ +1 +2 +3 +qux \ No newline at end of file diff --git a/tests/run/i14340.scala b/tests/run/i14340.scala new file mode 100644 index 000000000000..e9d8733ccfa5 --- /dev/null +++ b/tests/run/i14340.scala @@ -0,0 +1,20 @@ +class Foo(val value: Int) extends AnyVal +class Bar[A](val value: A) extends AnyVal + +class Container1 extends reflect.Selectable + +class Container2 extends Selectable: + def selectDynamic(name: String) = Bar(name) + +val cont1 = new Container1: + def foo = new Foo(1) + val bar = new Bar(Foo(2)) + def fooFromInt(i: Int) = new Foo(i) + +val cont2 = (new Container2).asInstanceOf[Container2 { def qux: Bar[String] }] + +@main def Test: Unit = + println(cont1.foo.value) + println(cont1.bar.value.value) + println(cont1.fooFromInt(3).value) + println(cont2.qux.value) \ No newline at end of file From 1a6224b8ced3b926733044e1c7700212303b3880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pa=C5=82ka?= Date: Wed, 15 Feb 2023 17:15:46 +0100 Subject: [PATCH 2/2] Preserve correct behaviour of structural selection for reflective and non-reflective access --- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../src/dotty/tools/dotc/typer/Dynamic.scala | 38 ++++++++---- tests/run/i14340.check | 10 +++- tests/run/i14340.scala | 59 +++++++++++++++---- 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 174244b4a456..5416c0719467 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -808,6 +808,8 @@ class Definitions { @tu lazy val ClassTagModule: Symbol = ClassTagClass.companionModule @tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply) + @tu lazy val ReflectSelectableTypeRef: TypeRef = requiredClassRef("scala.reflect.Selectable") + @tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest") @tu lazy val TypeTest_unapply: Symbol = TypeTestClass.requiredMethod(nme.unapply) @tu lazy val TypeTestModule_identity: Symbol = TypeTestClass.companionModule.requiredMethod(nme.identity) diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala index 5de9ca6ab341..92d4f678b15c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala +++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala @@ -216,15 +216,33 @@ trait Dynamic { def fail(reason: String): Tree = errorTree(tree, em"Structural access not allowed on method $name because it $reason") - fun.tpe.widen match { - case tpe: ValueType if ValueClasses.isDerivedValueClass(tpe.classSymbol) => - val underlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass).info.resultType.asSeenFrom(tpe, tpe.classSymbol) - val wrapped = structuralCall(nme.selectDynamic, Nil) - val resultTree = if wrapped.tpe.isExactlyAny then New(tpe, wrapped.cast(underlying) :: Nil) else wrapped - resultTree.cast(tpe) + extension (tree: Tree) + /** 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, + * only the JVM type after erasure can be obtained through reflection, e.g. + * + * class Foo(val i: Int) extends AnyVal + * class Reflective extends reflect.Selectable + * val reflective = new Reflective { + * def foo = Foo(1) // Foo at compile time, java.lang.Integer in reflection + * } + * + * Because of that reflective access cannot be implemented properly in `scala.reflect.SelectDynamic` itself + * because it's not known there if the value should be wrapped in a value class constructor call or not. + * 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. + */ + def maybeBoxingCast(tpe: Type) = + val maybeBoxed = + if ValueClasses.isDerivedValueClass(tpe.classSymbol) && qual.tpe <:< defn.ReflectSelectableTypeRef then + val genericUnderlying = ValueClasses.valueClassUnbox(tpe.classSymbol.asClass) + val underlying = tpe.select(genericUnderlying).widen.resultType + New(tpe, tree.cast(underlying) :: Nil) + else + tree + maybeBoxed.cast(tpe) + fun.tpe.widen match { case tpe: ValueType => - structuralCall(nme.selectDynamic, Nil).cast(tpe) + structuralCall(nme.selectDynamic, Nil).maybeBoxingCast(tpe) case tpe: MethodType => def isDependentMethod(tpe: Type): Boolean = tpe match { @@ -244,11 +262,7 @@ trait Dynamic { fail(i"has a parameter type with an unstable erasure") :: Nil else TypeErasure.erasure(tpe).asInstanceOf[MethodType].paramInfos.map(clsOf(_)) - val finalTpe = tpe.finalResultType - if ValueClasses.isDerivedValueClass(finalTpe.classSymbol) then - New(finalTpe, structuralCall(nme.applyDynamic, classOfs).cast(finalTpe) - .cast(ValueClasses.valueClassUnbox(finalTpe.classSymbol.asClass).info.resultType.asSeenFrom(finalTpe, finalTpe.classSymbol)) :: Nil) - else structuralCall(nme.applyDynamic, classOfs).cast(finalTpe) + structuralCall(nme.applyDynamic, classOfs).maybeBoxingCast(tpe.finalResultType) } // (@allanrenucci) I think everything below is dead code diff --git a/tests/run/i14340.check b/tests/run/i14340.check index db372c9502e9..e96afd98e6e9 100644 --- a/tests/run/i14340.check +++ b/tests/run/i14340.check @@ -1,4 +1,10 @@ 1 +1 +2 2 -3 -qux \ No newline at end of file +10 +10 +20 +20 +100 +100 \ No newline at end of file diff --git a/tests/run/i14340.scala b/tests/run/i14340.scala index e9d8733ccfa5..0670c7e471ac 100644 --- a/tests/run/i14340.scala +++ b/tests/run/i14340.scala @@ -1,20 +1,57 @@ +class Container1 extends reflect.Selectable + +class Container2(values: Map[String, Any], methods: Map[String, Int => Any]) extends Selectable: + def selectDynamic(name: String) = values(name) + def applyDynamic(name: String)(arg: Int) = methods(name)(arg) + class Foo(val value: Int) extends AnyVal class Bar[A](val value: A) extends AnyVal -class Container1 extends reflect.Selectable +object Helpers: + def foo = Foo(1) + def bar = Bar(Foo(2)) + def qux1 = Bar(new Container1 { def foo = Foo(10) }) + def qux2 = Bar(new Container2(Map("foo" -> Foo(20)), Map.empty).asInstanceOf[Container2 { def foo: Foo }]) + +@main def Test: Unit = + val cont1 = new Container1: + def foo = Helpers.foo + val bar = Helpers.bar + def qux1 = Helpers.qux1 + def qux2 = Helpers.qux2 + def fooFromInt(i: Int) = Foo(i) -class Container2 extends Selectable: - def selectDynamic(name: String) = Bar(name) + val cont2values = Map( + "foo" -> Helpers.foo, + "bar" -> Helpers.bar, + "qux1" -> Helpers.qux1, + "qux2" -> Helpers.qux2 + ) -val cont1 = new Container1: - def foo = new Foo(1) - val bar = new Bar(Foo(2)) - def fooFromInt(i: Int) = new Foo(i) + val cont2methods = Map( + "fooFromInt" -> { (i: Int) => Foo(i) } + ) -val cont2 = (new Container2).asInstanceOf[Container2 { def qux: Bar[String] }] + val cont2 = Container2(cont2values, cont2methods).asInstanceOf[Container2 { + def foo: Foo + def bar: Bar[Foo] + def qux1: Bar[Container1 { def foo: Foo }] + def qux2: Bar[Container2 { def foo: Foo }] + def fooFromInt(i: Int): Foo + }] + -@main def Test: Unit = println(cont1.foo.value) + println(cont2.foo.value) + println(cont1.bar.value.value) - println(cont1.fooFromInt(3).value) - println(cont2.qux.value) \ No newline at end of file + println(cont2.bar.value.value) + + println(cont1.qux1.value.foo.value) + println(cont2.qux1.value.foo.value) + + println(cont1.qux2.value.foo.value) + println(cont2.qux2.value.foo.value) + + println(cont1.fooFromInt(100).value) + println(cont2.fooFromInt(100).value)