get annotations from class in scala 3 macros
Asked Answered
G

1

0

i am writing a macro to get annotations from a 'Class'

inline def getAnnotations(clazz: Class[?]): Seq[Any] = ${ getAnnotationsImpl('clazz) }
def getAnnotationsImpl(expr: Expr[Class[?]])(using Quotes): Expr[Seq[Any]] =
  import quotes.reflect.*

  val cls = expr.valueOrError // error: value value is not a member of quoted.Expr[Class[?]]
  val tpe = TypeRepr.typeConstructorOf(cls)
  val annotations = tpe.typeSymbol.annotations.map(_.asExpr)
  Expr.ofSeq(annotations)

but i get an error when i get class value from expr parameter

@main def test(): Unit =
  val cls = getCls
  val annotations = getAnnotations(cls)

def getCls: Class[?] = Class.forName("Foo")

is it possible to get annotations of a Class at compile time by this macro ?!

Gyrfalcon answered 8/3, 2022 at 4:41 Comment(1)
Actually, there is even a way to evaluate a tree itself (not its source code). See update.Panay
P
0

By the way, eval for Class[_] doesn't work even in Scala 2 macros: c.eval(c.Expr[Class[_]](clazz)) produces

java.lang.ClassCastException: 
scala.reflect.internal.Types$ClassNoArgsTypeRef cannot be cast to java.lang.Class.

Class[_] is too runtimy thing. How can you extract its value from its tree ( Expr is a wrapper over tree)?

If you already have a Class[?] you should use Java reflection rather than Scala 3 macros (with Tasty reflection).


Actually, you can try to evaluate a tree from its source code (hacking multi-staging programming and implementing our own eval instead of forbidden staging.run). It's a little similar to context.eval in Scala 2 macros (but we evaluate from a source code rather than from a tree).

import scala.quoted.*

object Macro {
  inline def getAnnotations(clazz: Class[?]): Seq[Any] = ${getAnnotationsImpl('clazz)}

  def getAnnotationsImpl(expr: Expr[Class[?]])(using Quotes): Expr[Seq[Any]] = {
    import quotes.reflect.*

    val str = expr.asTerm.pos.sourceCode.getOrElse(
      report.errorAndAbort(s"No source code for ${expr.show}")
    )

    val cls = Eval[Class[?]](str)

    val tpe = TypeRepr.typeConstructorOf(cls)
    val annotations = tpe.typeSymbol.annotations.map(_.asExpr)
    Expr.ofSeq(annotations)
  }
}
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.{Driver, util}
import dotty.tools.io.{VirtualDirectory, VirtualFile}
import java.net.URLClassLoader
import java.nio.charset.StandardCharsets
import dotty.tools.repl.AbstractFileClassLoader

object Eval {
  def apply[A](str: String): A = {
    val content =
      s"""
         |package $$generated
         |
         |object $$Generated {
         |  def run = $str
         |}""".stripMargin
    val sourceFile = util.SourceFile(
      VirtualFile(
        name = "$Generated.scala",
        content = content.getBytes(StandardCharsets.UTF_8)),
      codec = scala.io.Codec.UTF8
    )

    val files = this.getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs 
  
    val depClassLoader = new URLClassLoader(files, null)

    val classpathString = files.mkString(":")

    val outputDir = VirtualDirectory("output")

    class DriverImpl extends Driver {
      private val compileCtx0 = initCtx.fresh
      val compileCtx = compileCtx0.fresh
        .setSetting(
          compileCtx0.settings.classpath,
          classpathString
        ).setSetting(
        compileCtx0.settings.outputDir,
        outputDir
      )
      val compiler = newCompiler(using compileCtx)
    }

    val driver = new DriverImpl

    given Context = driver.compileCtx

    val run = driver.compiler.newRun
    run.compileSources(List(sourceFile))

    val classLoader = AbstractFileClassLoader(outputDir, depClassLoader)

    val clazz = Class.forName("$generated.$Generated$", true, classLoader)
    val module = clazz.getField("MODULE$").get(null)
    val method = module.getClass.getMethod("run")
    method.invoke(module).asInstanceOf[A]
  }
}
package mypackage

import scala.annotation.experimental

@experimental
class Foo
Macro.getAnnotations(Class.forName("mypackage.Foo")))

// new scala.annotation.internal.SourceFile("/path/to/src/main/scala/mypackage/Foo.scala"), new scala.annotation.experimental()
scalaVersion := "3.1.3"

libraryDependencies += scalaOrganization.value %% "scala3-compiler" % scalaVersion.value

How to compile and execute scala code at run-time in Scala3?

(compile time of the code expanding macros is the runtime of macros)


Actually, there is even a way to evaluate a tree itself (not its source code). Such functionality exists in Scala 3 compiler but is deliberately blocked because of phase consistency principle. So this to work, the code expanding macros should be compiled with a compiler patched

https://github.com/DmytroMitin/dotty-patched

scalaVersion := "3.2.1"
libraryDependencies += scalaOrganization.value %% "scala3-staging" % scalaVersion.value
// custom Scala settings
managedScalaInstance := false
ivyConfigurations += Configurations.ScalaTool
libraryDependencies ++= Seq(
  scalaOrganization.value  %  "scala-library"  % "2.13.10",
  scalaOrganization.value  %% "scala3-library" % "3.2.1",
  "com.github.dmytromitin" %% "scala3-compiler-patched-assembly" % "3.2.1" % "scala-tool"
)
import scala.quoted.{Expr, Quotes, staging, quotes}

object Macro {
  inline def getAnnotations(clazz: Class[?]): Seq[String] = ${impl('clazz)}

  def impl(expr: Expr[Class[?]])(using Quotes): Expr[Seq[String]] = {
    import quotes.reflect.*
    given staging.Compiler = staging.Compiler.make(this.getClass.getClassLoader)

    val tpe = staging.run[Any](expr).asInstanceOf[TypeRepr]

    val annotations = Expr(tpe.typeSymbol.annotations.map(_.asExpr.show))
    report.info(s"annotations=${annotations.show}")
    annotations
  }
}

Normally, for expr: Expr[A] staging.run(expr) returns a value of type A. But Class is specific. For expr: Expr[Class[_]] inside macros it returns a value of type dotty.tools.dotc.core.Types.CachedAppliedType <: TypeRepr. That's why I had to cast.

In Scala 2 this also would be c.eval(c.Expr[Any](/*c.untypecheck*/(clazz))).asInstanceOf[Type].typeSymbol.annotations because for Class[_] c.eval returns scala.reflect.internal.Types$ClassNoArgsTypeRef <: Type.

https://github.com/scala/bug/issues/12680

Panay answered 19/9, 2022 at 12:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.