Scala macro to print code?
Asked Answered
C

3

6

I want to do something like this:

def assuming[A](condition: => Boolean)(f: => A): A = {
  require(condition, /* print source-code of condition */)
  f
}

Sample usage:

def fib(n: Int) = n match { // yes, yes, I know this is not efficient
  case 0 => 0 
  case 1 => 1
  case i => assuming(i > 0) { fib(i-1) + fib(i-2) }
}

Now, for example, if you call fib(-20), I want it to throw an exception with a message like Assertion failed: -20 > 0 or Assertation failed: i > 0

Crocus answered 24/4, 2014 at 6:10 Comment(0)
B
5

Dude, isn't an assert macro one of the basic use cases you implement to learn how to use macros?

Well, that's what I thought, too.

By "glean snippets" in my other answer I meant what specs2 does in its s2 macro.

Or, you can do an arbitrary representation, as in my variant rip-off of expecty.

I thought I'd type your example into REPL, in a couple of lines. After all, you're just trying to print the snippet from the source that corresponds to the tree representing your conditional.

What could be easier?

Of course, it's easier under -Yrangepos, but we can postulate positions.

I'm willing to share how far I got before I lost interest.

People (e.g., paulp, who is the vox paulpuli) want trees to have attachments representing "the source I typed on my keyboard", because, you know, maybe I want it for a message or to figure out what the user was trying to accomplish.

It looks like the predicate p doesn't have a range position. So the other idea is that we know the start of the macro application, which is the paren of the second param list, so working backward through the source, matching the closing paren of the first param list, is doable.

Note that showCode isn't helpful because for a conditional like 10 < 5 it shows false, neatly folded.

object X {
  import reflect.macros.blackbox.Context
  def impl[A: c.WeakTypeTag](c: Context)(p: c.Expr[Boolean])(body: c.Expr[A]) = {
    import c.universe._
    def treeLine(t: Tree): String = lineAt(t.pos)
    def lineAt(pos: Position): String = if (pos.isRange) pos.lineContent.drop(pos.column - 1).take(pos.end - pos.start + 1) else "???"
    val msg =
      if (p.tree.pos.isRange) {  // oh, joy
        treeLine(p.tree)
      } else {
        /*
        Console println s"content ${p.tree.pos.lineContent}"
        Console println s"column ${p.tree.pos.column}"  // alas, that's the column of the point of the top of the tree, e.g., < in "a < b".
        val len = body.tree.pos.start - p.tree.pos.start
        p.tree.pos.lineContent drop (p.tree.pos.column - 1) take len
        */
        // OK, I get it: positions are a big mystery. Make woo-woo ghost noises.
        // What we do know is the start of the apply, which must have a close paren or brace in front of it to match:
        // apply(condition)(body)
        showCode(p.tree)
      }
    q"require($p, $msg) ; $body"
  }
  def x[A](p: Boolean)(body: =>A): A = macro X.impl[A]
}

It just occurred to me to get the rangy position this way:

object X {
  import reflect.macros.blackbox.Context
  def impl(c: Context)(p: c.Expr[Boolean]) = {
    import c.universe._
    def lineAt(pos: Position): String = if (pos.isRange) pos.lineContent.drop(pos.column - 1).take(pos.end - pos.start + 1) else "???" 
    val msg = lineAt(c.macroApplication.pos)  // oh, joy
    q"require($p, $msg) ; new { def apply[A](body: =>A): A = body }"
  }
  def x(p: Boolean): { def apply[A](body: =>A): A } = macro X.impl
}

That's close on usage x(10 < 5)(println("hi")): requirement failed: (10 < 5)(p. Margin for error.

Butyraldehyde answered 1/5, 2014 at 11:9 Comment(1)
Scala macros codes are so unreadable/undebuggable... Can't wait for this to come out: scalamacros.org/paperstalks/…Crocus
B
5

Have you consulted the docs at:

http://www.scala-lang.org/api/2.11.0/scala-reflect/#scala.reflect.api.Printers

scala> show(q"-1 < 0")
res6: String = -1.$less(0)

scala> showCode(q"-1 < 0")
res7: String = (-1).<(0)

Alternatively, folks have used source positions to glean snippets to print.

Butyraldehyde answered 24/4, 2014 at 6:45 Comment(0)
B
5

Dude, isn't an assert macro one of the basic use cases you implement to learn how to use macros?

Well, that's what I thought, too.

By "glean snippets" in my other answer I meant what specs2 does in its s2 macro.

Or, you can do an arbitrary representation, as in my variant rip-off of expecty.

I thought I'd type your example into REPL, in a couple of lines. After all, you're just trying to print the snippet from the source that corresponds to the tree representing your conditional.

What could be easier?

Of course, it's easier under -Yrangepos, but we can postulate positions.

I'm willing to share how far I got before I lost interest.

People (e.g., paulp, who is the vox paulpuli) want trees to have attachments representing "the source I typed on my keyboard", because, you know, maybe I want it for a message or to figure out what the user was trying to accomplish.

It looks like the predicate p doesn't have a range position. So the other idea is that we know the start of the macro application, which is the paren of the second param list, so working backward through the source, matching the closing paren of the first param list, is doable.

Note that showCode isn't helpful because for a conditional like 10 < 5 it shows false, neatly folded.

object X {
  import reflect.macros.blackbox.Context
  def impl[A: c.WeakTypeTag](c: Context)(p: c.Expr[Boolean])(body: c.Expr[A]) = {
    import c.universe._
    def treeLine(t: Tree): String = lineAt(t.pos)
    def lineAt(pos: Position): String = if (pos.isRange) pos.lineContent.drop(pos.column - 1).take(pos.end - pos.start + 1) else "???"
    val msg =
      if (p.tree.pos.isRange) {  // oh, joy
        treeLine(p.tree)
      } else {
        /*
        Console println s"content ${p.tree.pos.lineContent}"
        Console println s"column ${p.tree.pos.column}"  // alas, that's the column of the point of the top of the tree, e.g., < in "a < b".
        val len = body.tree.pos.start - p.tree.pos.start
        p.tree.pos.lineContent drop (p.tree.pos.column - 1) take len
        */
        // OK, I get it: positions are a big mystery. Make woo-woo ghost noises.
        // What we do know is the start of the apply, which must have a close paren or brace in front of it to match:
        // apply(condition)(body)
        showCode(p.tree)
      }
    q"require($p, $msg) ; $body"
  }
  def x[A](p: Boolean)(body: =>A): A = macro X.impl[A]
}

It just occurred to me to get the rangy position this way:

object X {
  import reflect.macros.blackbox.Context
  def impl(c: Context)(p: c.Expr[Boolean]) = {
    import c.universe._
    def lineAt(pos: Position): String = if (pos.isRange) pos.lineContent.drop(pos.column - 1).take(pos.end - pos.start + 1) else "???" 
    val msg = lineAt(c.macroApplication.pos)  // oh, joy
    q"require($p, $msg) ; new { def apply[A](body: =>A): A = body }"
  }
  def x(p: Boolean): { def apply[A](body: =>A): A } = macro X.impl
}

That's close on usage x(10 < 5)(println("hi")): requirement failed: (10 < 5)(p. Margin for error.

Butyraldehyde answered 1/5, 2014 at 11:9 Comment(1)
Scala macros codes are so unreadable/undebuggable... Can't wait for this to come out: scalamacros.org/paperstalks/…Crocus
S
1

If you are using Scala 2.11.x the best way to go is the showCode method. This method will correctly print an arbitrary Scala tree. For example:

scala> import reflect.runtime.universe._
import reflect.runtime.universe._

showCode(q"3.14 < 42")
res1: String = 3.14.<(42)

In the previous versions of Scala you would have to use the method show which does not guarantee correctness:

scala> show(q"3.14 < 42")
res2: String = 3.14.$less(42)

The method showCode was designed with correctness in mind so it will not necessarily print beautiful code. If beauty is of importance for you can either contribute to the Scala Printers or you can write your own printer. Another interesting printer for Scala trees is the PrettyPrinter from Scala Refactoring.

Sevenup answered 1/5, 2014 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.