Message Serializer Generation

This commit changes the project structure so you need to either recreate your IDEA project or refresh it.

Scala Macro is a technique by which we can hook into one of the phases of compilation and modify the syntax tree before it is turned into code.

This idea is extensively used in LISP which has the benefit of being homoiconic. A LISP program is itself a list and can be manipulated by the program using its extensive list operators.

Scala doesn't have this advantage so macros are a bit harder to use but the idea is the same. We define a macro annotation by

@compileTimeOnly("enable macro paradise to expand macro annotations")
class MessageMacro extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro MessageMacro.impl
}

The @compileTimeOnly is there to ensure that the macro is properly expanded. If the project isn't properly setup to use macros, the @MessageMacro is a normal annotation and would have no effect. @compileTimeOnly emits a compile-time error because the @MessageMacro should have been expanded.

The macro keyword introduces the function that receives the AST and will transform it.

Let's say we use it on Ping.

@MessageMacro case class Ping(nonce: Long)

The signature of the macro implementation is:

object MessageMacro {
  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = ???

It receives a compiler context and a list of annottees that correspond to the annotated items. In this case the case class Ping. The result should be a transformed Expr tree. Now, it's possible to directly work with the trees but because Scala is a fairly complex language, the AST is quite complex too.

There is an easier way to manipulate trees called quasi-quoting coming from LISP.

In this example where classDef is an expression that matches a class definition

val q"case class $messageName(..$fields)" = classDef

The q prefixing the string marks the quasi-quote that is pattern matched against the class definition. If it matches, the variable messageName is bound to the sub tree that represents the class name and $fields is bound to a list of fields.

When the quasi-quote is used on the right side of a statement, it builds an expression by inserting the values of the quoted variables.

For example, this builds the output expressions

q"""case class $messageName(..$fields) extends BitcoinMessage {
  import BitcoinMessage.ByteStringBuilderExt
  val command = $messageNameLC
  def toByteString(): ByteString = {
    val bb = new ByteStringBuilder
    ..$writeBody
    bb.result()
  }
}

object ${messageName.toTermName} extends ByteOrderImplicit {
  import BitcoinMessage.ByteStringIteratorExt
  def parse(bs: ByteString) = parseBI(bs.iterator)
  def parseBI(bi: ByteIterator) = {
    ..$readBody
    new $messageName(..$fieldNames)
  }
}"""

quasi-quoting is a very powerful feature and greatly simplifies working with ASTs but there are still times where we have to work with the trees directly. The scala-reflect package has the API for the AST data model and provides many manipulations and transformations functions.