Scala macros from lisp macro perspective


#1

I have been writing Lisp macros for 20 years or more, and I’m now taking a look into Scala macros. But there are some things which are puzzling. In Lisp, macros take advantage of the homoiconicity of the language, i.e., that the code is a data structure which code can manipulate. It appears to me that Scala macros take an argument (perhaps some arguments, not sure) which are ASTs. So how can I pass literal values to a macro? Must any value I pass be valid Scala code? If so, have all the macros within that code already been expanded?

In Lisp (in particular Common Lisp, but also in emacs lisp) the operands of the macro need not be valid lisp code, in the sense that it need not be compilable as is, but rather is simply a data structure of what the user typed. If the user provided a malformed if/then/else for example, then the macro will see the malformation. The macro writer can and often does take advantage of this feature in allowing macros to extend the syntax. For trivial example, I could write a macro which allows the user to use if/else/then …

One possibility I though about to work around this Scala limitation is to pass an object to the macro, and let the macro inspect the object to make its decisions. However, as I understand it, the macro would not actually receive the object, but instead would receive an AST which evaluated at runtime would produce the object. How can I access the actual object within the macro? Do I need to somehow EVAL the AST?

I’d appreciate any help someone can provide. I’m also happy to give more details about the particular application I’m trying to implement.


#2

Literals can be accessed in the macro like this:

scala> def isLiteralImpl(c: Context)(i: c.Tree): c.Tree = {
     |   import c.universe._
     |   i match {
     |     case Literal(Constant(lit: Int)) => q"true"
     |     case _ => q"false"
     |   }
     | }
isLiteralImpl: (c: scala.reflect.macros.blackbox.Context)(i: c.Tree)c.Tree

scala> def isLiteral(i: Int): Boolean = macro isLiteralImpl
defined term macro isLiteral: (i: Int)Boolean

scala> isLiteral(3)
res1: Boolean = true

scala> val a = 5
a: Int = 5

scala> isLiteral(a)
res2: Boolean = false

All the code you pass to a macro must parse and typecheck as valid code. That means you cannot pass a malformed if/then/else expression, but you can still pass a well-formed if/then/else and let your macro rewrite that code into something with different semantics. And with the help of infix notation, implicit conversions, etc. you can still get a lot of things to typecheck in scala. For an example take a look at this proof of concept I wrote a while ago: https://github.com/Jasper-M/simple-lenses/blob/master/src/main/scala/simplelenses/simple-lenses.scala

You could potentially evaluate the AST at compile time, but you might also just emit an AST containing the necessary calls on the object, delaying the actual decisions until runtime, depending on your usecase.


#3

Hi Jasper, thanks for the help. wrt evaluating the AST what I want to do is let the macro look at the object, and emit different code depending on the value of the evaluated object. In lisp, 99.99999% of the time if the person writing the macro things he needs to call eval, he’s doing something wrong. But in this case, I’m not sure how to avoid it.

Imagine the following scenario. I want a parameter to the macro to be an object which represents a regular expression, I want the macro to parse the regular expression and generate a finite state machine, and finally emit code to implement the state machine. There will be one local function for each state, and each local function will have one tail-call of another such function for each transition that state has in the state machine. Local functions representing states which are final states will check whether they’ve reached the end of the sequence, and return. So the code my macro emits needs to depend on the evaluated state machine object. I really want different local functions emitted spending on the state machine itself.

Implementing such a macro in lisp causes the compiler to create code which is 100s of times faster than if I write a general purpose function which walks the state machine at run-time.

The difference in lisp is that the macro just takes a hierarchical list (homo-iconic) which is allocated at parse time which I can easily walk from within the macro. E.g., (:repeat (:concat (:or “a” “b”) (:or “c” “d”) (:optional (:or “e” “f” “g”)))) ---- my idea in scala was to use type constructors in place of :repeat :concat :or :optional etc…

The original paper is here https://www.lrde.epita.fr/dload/papers/newton.16.els.pdf


#4

HI Jasper, you mentioned how to deal with literals like numbers and booleans, but can I use structured literals such as a list or tuple? for example can I pass something like the following at the macro call site? (:repeat (:or “a” “b”) (:or “c” “d” “e” “f”) (:optional “x”))

Is there some similar concept in Scala to a quoted list of arbitrary structure?


#5

re: using a list, Scala code such as List(1, 4, 9) (short for List.apply(1, 4, 9)) produces a particular AST structure that your macro is free to detect and then interpret or rewrite however it likes at compile time.

my idea in scala was to use type constructors in place of :repeat :concat :or :optional etc

That sounds like a good approach. Your macro could detect code consisting of nested calls to Repeat.apply, Concat.apply, and so forth and handle them appropriately.