Search code examples
scalamacrosscala-macrosscala-macro-paradise

Scala annotation macro only works with pre-defined classes


Note: There's an EDIT below! Note: There's another EDIT below!

I have written a Scala annotation macro that is being passed a class and creates (or rather populates) a case object. The name of the case object is the same as the name of the passed class. More importantly, for every field of the passed class, there will be a field in the case object of the same name. The fields of the case object, however, are all of type String, and their value is the name of the type of the respective field in the passed class. Example:

// Using the annotation macro to populate a case object called `String`
@RegisterClass(classOf[String]) case object String

// The class `String` defines a field called `value` of type `char[]`.
// The case object also has a field `value`, containing `"char[]"`.
println(String.value) // Prints `"char[]"` to the console

This, however, seems to only work with pre-defined classes such as String. If I define a case class A(...) and try to do @RegisterClass(classOf[A]) case object A, I get the following error:

[info]   scala.tools.reflect.ToolBoxError: reflective compilation has failed:
[info]   
[info]   not found: type A

What have I done wrong? The code of my macro can be found below. Also, if someone notices un-idiomatic Scala or bad practices in general, I wouldn't mind a hint. Thank you very much in advance!

class RegisterClass[T](clazz: Class[T]) extends StaticAnnotation {
  def macroTransform(annottees: Any*) =
    macro RegisterClass.expandImpl[T]
}

object RegisterClass {
  def expandImpl[T](c: blackbox.Context)(annottees: c.Expr[Any]*) = {
    import c.universe._
    val clazz: Class[T] = c.prefix.tree match {
      case q"new RegisterClass($clazz)" => c.eval[Class[T]](c.Expr(clazz))
      case _ =>  c.abort(c.enclosingPosition, "RegisterClass: Annotation expects a Class[T] instance as argument.")
    }
    annottees.map(_.tree) match {
      case List(q"case object $caseObjectName") =>
        if (caseObjectName.toString != clazz.getSimpleName)
          c.abort(c.enclosingPosition, "RegisterClass: Annotated case object and class T of passed Class[T] instance" +
            "must have the same name.")
        val clazzFields = clazz.getDeclaredFields.map(field => field.getName -> field.getType.getSimpleName).toList
        val caseObjectFields = clazzFields.map(field => {
          val fieldName: TermName = field._1
          val fieldType: String = field._2
          q"val $fieldName = $fieldType"
        })
        c.Expr[Any](q"case object $caseObjectName { ..$caseObjectFields }")
      case _ => c.abort(c.enclosingPosition, "RegisterClass: Annotation must be applied to a case object definition.")
    }
  }
}

EDIT: As Eugene Burmako pointed out, the error happens because class A hasn't been compiled yet, so a java.lang.Class for it doesn't exist. I have now started a bounty of 100 StackOverflow points for everyone who as an idea how one could get this to work!

EDIT 2: Some background on the use case: As part of my bachelor thesis I am working on a Scala DSL for expressing queries for event processing systems. Those queries are traditionally expressed as strings, which induces a lot of problems. A typical query would look like that: "select A.id, B.timestamp from pattern[A -> B]". Meaning: If an event of type A occurs and after that an event of type B occurs, too, give me the id of the A event and the timestamp of the B event. The types A and B usually are simple Java classes over which I have no control. id and timestamp are fields of those classes. I would like queries of my DSL to look like that: select (A.id, B.timestamp) { /* ... * / }. This means that for every class representing an event type, e.g., A, I need a companion object -- ideally of the same name. This companion object should have the same fields as the respective class, so that I can pass its fields to the select function, like so: select (A.id, B.timestamp) { /* ... * / }. This way, if I tried to pass A.idd to the select function, it would fail at compile-time if there was no such field in the original class -- because then there would not be one in the companion object either.


Solution

  • This isn't an answer to your macro problem, but it could be a solution to your general problem.
    If you can allow a minor change to the syntax of your DSL this might be possible without using macro's (depending on other requirements not mentioned in this question).

    scala> class Select[A,B]{
         |   def apply[R,S](fa: A => R, fb: B => S)(body: => Unit) = ???
         | }
    defined class Select
    
    scala> def select[A,B] = new Select[A,B]
    select: [A, B]=> Select[A,B]
    
    scala> class MyA { def id = 42L }
    defined class MyA
    
    scala> class MyB { def timestamp = "foo" }
    defined class MyB
    
    scala> select[A,B](_.id, _.timestamp){ /* ... */ }
    scala.NotImplementedError: an implementation is missing
    

    I use the class Select here as a means to be able to specify the types of your event classes while letting the compiler infer the result types of the functions fa and fb. If your don't need those result types you could just write it as def select[A,B](fa: A => Any, fb: B => Any)(body: => Unit) = ???.

    If necessary you can still implement the select or apply method as a macro. But using this syntax, you will no longer need to generate objects with macro annotations.