Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Macro to access source code text at runtime

Tags:

macros

scala

Is there already or is it possible to have a Scala macro that gives me access to the text of the source? For instance I would like to write code like this:

val list = List(1, 2, 3)
val (text, sum) = (list.sum).withSource{(source, sum) => (source, sum)}
// would return ("list.sum", 6)
(list.sum).withSource{(source, sum) => println(s"$source: $sum"}
// prints list.sum: 6
like image 536
huynhjl Avatar asked Apr 12 '13 13:04

huynhjl


2 Answers

Do you really want a source code or Tree is enough?

For Tree you could use prefix of Context like this:

import scala.language.experimental.macros
import reflect.macros.Context

implicit class WithSourceHelper[T](source: T) {
  def withSource[R](f: (String, T) => R): R = macro withSourceImpl[T, R]
}

def withSourceImpl[T, R](c: Context)(f: c.Expr[(String, T) => R]): c.Expr[R] = {
  import c.universe.{reify, Apply}

  val source = c.prefix.tree match {
    case Apply(_, List(s)) => s
    case _ => c.abort(c.enclosingPosition, "can't find source")
  }

  reify{ f.splice.apply(c.literal(source.toString).splice, c.Expr[T](source).splice) }
}

Usage:

scala> val (x, y) = (1, 2)
x: Int = 1
y: Int = 2

scala> {x + y}.withSource{ (s, r) => s"$s = $r" }
res15: String = x.+(y) = 3

scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)

scala> val (text, sum) = (list.sum).withSource{(source, sum) => (source, sum)}
text: String = list.sum[Int](math.this.Numeric.IntIsIntegral)
sum: Int = 6

scala> (list.sum).withSource{(source, sum) => println(s"$source: $sum")}
$line38.$read.$iw.$iw.$iw.list.sum[Int](math.this.Numeric.IntIsIntegral): 6
like image 133
senia Avatar answered Nov 16 '22 04:11

senia


I was not able to reuse withSource directly to just print the source and value and return the value. The withSource macro cannot be utilized from the same object itself (so I cannot just add my slightly modified version of withSource right in that file) and I am not able to call withSource from a subclass of WithSourceHelper, limiting reuse through inheritance.

In case anyone is interested, here is a complement to Senia's answer to just log the value with the source and return the value so the rest of the computation can occur.

def logValueImpl[T](c: Context): c.Expr[T] = {
  import c.universe._
  val source = c.prefix.tree match {
    case Apply(_, List(s)) => s
    case _ => c.abort(c.enclosingPosition, "can't find source")
  }
  val freshName = newTermName(c.fresh("logValue$"))
  val valDef = ValDef(Modifiers(), freshName, TypeTree(source.tpe), source)
  val ident = Ident(freshName)
  val print = reify{
    println(c.literal(show(source)).splice + ": " + c.Expr[T](ident).splice) }
  c.Expr[T](Block(List(valDef, print.tree), ident))
}

I then define it as an implicit conversion on def p = macro Debug.logValueImpl[T]. I can then use like this:

List(1, 2, 3).reverse.p.head 
// prints: immutable.this.List.apply[Int](1, 2, 3).reverse: List(3, 2, 1)

The funny part is that I can apply it twice:

List(1, 2, 3).reverse.p.p

And it will show me what the logValueImpl macro did:

{
  val logValue$7: List[Int] = immutable.this.List.apply[Int](1, 2, 3).reverse;
  Predef.println("immutable.this.List.apply[Int](1, 2, 3).reverse: ".+(logValue$7));
  logValue$7
}

It seems to work with other macros as well:

f"float ${1.3f}%3.2f; str ${"foo".reverse}%s%n".p`
//prints:
{
  val arg$1: Float = 1.3;
  val arg$2: Any = scala.this.Predef.augmentString("foo").reverse;
  scala.this.Predef.augmentString("float %3.2f; str %s%%n").format(arg$1, arg$2)
}: float 1.30; str oof%n

Even more interestingly if I used showRaw instead of show I can even see the tree of the expanded macro, which may come handy to figure out how to write other macros.

like image 21
huynhjl Avatar answered Nov 16 '22 06:11

huynhjl