scala3
scala3 copied to clipboard
Reflection: no exported way to access `TypeDef#rhs` / `TypeRef#underlying` on a TypeRepr
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 qctx: Quotes): Expr[(String, String)] = {
import qctx.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 = qctx.getClass.getMethods.collect { case m if m.getName == "ctx" => m }.head
val underlying = underlyingMethod.invoke(typeRepr, quotesImplCtxMethod.invoke(qctx))
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
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.
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.
Indeed, it seems that we should add the underlying methods to TypeRef.
Performance hint
- '{ (${ Expr(defDefRhsTpe) }, ${ Expr(underlyingTpe) }) }
+ Expr((defDefRhsTpe, underlyingTpe))
@nicolasstucki
ParamRef.underlying is also needed (https://github.com/lampepfl/dotty/issues/16734)