scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.83k stars 1.05k forks source link

Reflection: no exported way to access `TypeDef#rhs` / `TypeRef#underlying` on a TypeRepr #15799

Open neko-kai opened 2 years ago

neko-kai commented 2 years ago

Compiler version

3.1.3

Minimized code

example.scala:

package izumi.reflect.dottyreflection

@main def main = {
  object X {
    type Alias = String
    opaque type Opaque = String
    type Bound <: String
    opaque type OpaqueBound <: String = String
  }
  import X.*

  println(s"Alias: ${exampleMacro.typeDefRhs[Alias]}")
  println(s"Opaque: ${exampleMacro.typeDefRhs[Opaque]}")
  println(s"Bound: ${exampleMacro.typeDefRhs[Bound]}")
  println(s"OpaqueBound: ${exampleMacro.typeDefRhs[OpaqueBound]}")
}

exampleMacro.scala:

package izumi.reflect.dottyreflection

import scala.quoted.*

object exampleMacro {

  inline def typeDefRhs[T <: AnyKind]: (String, String) = ${ typeDefRhsImpl[T] }

  def typeDefRhsImpl[T <: AnyKind: Type](using Quotes): Expr[(String, String)] = {
    import quotes.reflect.*
    val typeRepr = TypeRepr.of[T]
    val typeSymbol = typeRepr.typeSymbol
    val defDefRhsTpe: String = typeSymbol.tree match {
      case t: TypeDef => t.rhs.asInstanceOf[TypeTree].tpe.show
    }
    val underlyingTpe: String = {
      // TypeRef#underlying(using Context)
      val underlyingMethod = typeRepr.getClass.getMethods.collect { case m if m.getName == "underlying" => m }.head
      // QuotesImpl#ctx: Context
      val quotesImplCtxMethod = quotes.getClass.getMethods.collect { case m if m.getName == "ctx" => m }.head

      val underlying = underlyingMethod.invoke(typeRepr, quotesImplCtxMethod.invoke(quotes))
      underlying.asInstanceOf[TypeRepr].show
    }
    '{ (${ Expr(defDefRhsTpe) }, ${ Expr(underlyingTpe) }) }
  }

}

Output

Alias: (_ >: scala.Predef.String <: scala.Predef.String,_ >: scala.Predef.String <: scala.Predef.String)
Opaque: (_ >: scala.Nothing <: scala.Any,_ >: scala.Nothing <: scala.Any)
Bound: (_ >: scala.Nothing <: scala.Predef.String,_ >: scala.Nothing <: scala.Predef.String)
OpaqueBound: (_ >: scala.Nothing <: scala.Predef.String,_ >: scala.Nothing <: scala.Predef.String)

Expectation

We need to get the right hand side of type alias / opaque / abstract type to to construct a Type Tag from it, but the only way to do this right now is by using Symbol#tree in typeRepr.typeSymbol.tree match { case t: TypeDef => t.rhs.asInstanceOf[TypeTree].tpe }

Unfortunately, Symbol#tree sometimes requires -Yretain-trees to work, which creates problems for end-users.

However, the right hand side is contained in a TypeRepr's TypeRef#underlying[1] method. But since the method is not exported in Quotes.scala, the only way to access this right now is through java reflection.

Original issue: https://github.com/zio/izumi-reflect/issues/307

neko-kai commented 2 years ago

Correction: typeRef.typeSymbol.owner.typeRef.memberType(typeRef.typeSymbol) seems to also produce the same output as TypeRef#underlying / TypeDef#rhs, but I'm not sure how reliable this is.

neko-kai commented 1 year ago

typeRef.typeSymbol.owner.typeRef.memberType(typeRef.typeSymbol) seems to also produce the same output as TypeRef#underlying / TypeDef#rhs, but I'm not sure how reliable this is.

No, It's not a reliable substitution. When used on a TypeParamRef it returns Any instead of the underlying TypeBounds

btw access to underlying method on ParamRef would be great too.

nicolasstucki commented 1 year ago

Indeed, it seems that we should add the underlying methods to TypeRef.

nicolasstucki commented 1 year ago
Performance hint
-    '{ (${ Expr(defDefRhsTpe) }, ${ Expr(underlyingTpe) }) }
+    Expr((defDefRhsTpe, underlyingTpe))
neko-kai commented 1 year ago

@nicolasstucki ParamRef.underlying is also needed (https://github.com/lampepfl/dotty/issues/16734)