0
votes

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.

1
I don't know if this actually works, but perhaps it helps: What if you add a field to the case object. I.e., @RegisterClass case object String { type T = String } or @RegisterClass case object String { val t : String = _ }. Then possibly the annotation macro can get access to the type T or the type of t by inspecting the annottee? (I don't know if this works, that's why I don't post an answer, but perhaps the idea helps somehow.)Dominique Unruh
Couple thoughts: (1) Rewriting objects on such a large scale is a Bad Idea since you are essentially hiding/overwriting existing functionality. (2) Spark 2.0 does something similar (but not type-checked, which would be nice) with columns inside operations like select, join, groupby (see the examples here). (3) Why not make a compiler plugin for this? Then you can "rewrite" Scala syntax locally.Alec

1 Answers

1
votes

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.