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 ?!

1
  • Actually, there is even a way to evaluate a tree itself (not its source code). See update. Commented Oct 30, 2022 at 7:52

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.