11

How can parameters/settings be passed to a Scala macro?

These settings should not be global, but per macro invocation.

What I would like to have is something similar to this:

def a(param: Int) = macro internalMacro("setting 1")
def b(param: Int) = macro internalMacro("setting 2")

whereas setting 1 and setting 2 should then be constant values, accessible from within the macro, so I can make the internal behavior dependent on them.

Emiswelt
  • 3,909
  • 1
  • 38
  • 56

1 Answers1

14

The parameter lists of your method and the macro definition have to line up exactly, with the macro definition method having an extra initial parameter list for the context, and every other parameter from the method definition having the same name and the same type wrapped in a c.Expr. (Note however that the type parameter lists can differ.)

This means that you can't pass information to the macro implementation as arguments in your method definition. You can use a static annotation to accomplish the same thing, though (this is a trick I learned from Eugene Burmako, who used it to implement structural types without reflective access, a.k.a. vampire methods):

import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

class setting(value: String) extends StaticAnnotation

def internalMacro(c: Context)(param: c.Expr[Int]) = {
  import c.universe._
  val settingValue = c.macroApplication.symbol.annotations.filter(
    _.tree.tpe <:< typeOf[setting]
  ).headOption.flatMap(
    _.tree.children.tail.collectFirst {
      case Literal(Constant(s: String)) => s
    }
  ).getOrElse(
    c.abort(c.enclosingPosition, "Annotation body not provided!")
  )
  settingValue match {
    case "setting 1" => c.Expr(q"42")
    case _ => param
  }
}

And then:

scala> @setting("setting 1") def a(param: Int): Int = macro internalMacro
defined term macro a: (param: Int)Int

scala> @setting("setting 2") def b(param: Int): Int = macro internalMacro
defined term macro b: (param: Int)Int

scala> def c(param: Int): Int = macro internalMacro
defined term macro c: (param: Int)Int

scala> a(10)
res0: Int = 42

scala> b(10)
res1: Int = 10

scala> c(10)
<console>:22: error: Annotation body not provided!
              c(10)
               ^

And I didn't even examine any enclosing trees. See my blog post here for an example of this approach in action.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Does this have any advantages over providing multiple macro wrapper impls (ie. `internalMacroSetting1`, `internalMacroSetting2`) which delegate (with argument) to the full impl? – Miles Sabin Aug 09 '14 at 15:36
  • It makes the parametrization clearer where it matters, and if you wrap up the annotation reading stuff once there can be less repetition / boilerplate (especially if you have lots of parameters). – Travis Brown Aug 09 '14 at 16:42
  • I can see it's a win if there are multiple parameters. I'm not completely convinced in the single parameter case. – Miles Sabin Aug 09 '14 at 19:21
  • 1
    Sure, I don't think it's overwhelmingly better, but if e.g. your macro definitions are in a separate object, using this approach lets you see the difference between two macro methods without guessing from names, jumping over to the macro definitions, etc. – Travis Brown Aug 09 '14 at 19:25
  • @MilesSabin multiple macro impls (one for each setting) will only work if the set of possible settings is finite (and reasonably small). – Tomas Mikula Apr 14 '18 at 10:41
  • @TomasMikula I'd love to see a program with an infinite set of definitions `@setting("one")`, `@setting("two")`, etc. ... ;-) How long do you think it would take to compile? – Miles Sabin Apr 15 '18 at 14:20
  • @MilesSabin :D Any one program will have a finite set of settings, but there can be a potentially infinite number of programs using the same macro. – Tomas Mikula Apr 15 '18 at 22:32