Mike Beckerle
Mike Beckerle

Reputation: 847

scala macro that refers to 'this' object

I am trying to use a macro to eliminate the need for scala to construct a downward-passed function object. This code gets used in inner-loops of our system, and we don't want the inner loop to just allocate objects endlessly. This is creating performance problems for us.

Our original code was this:

dis.withBitLengthLimit(newLimit){... body ...}

And the body was a function that was passed in as a function object.

The problem I have is that the original non-macro version refers to 'this'. My workaround below is to make each place the macro is called pass the 'this' object as another argument. i.e., ugly like:

dis.withBitLengthLimit(dis, newLimit){... body ...}

It's not awful, but sure seems like passing dis should be unnecessary.

Is there a cleaner way?

Here's the macro below.

object IOMacros {
  /**
   * Used to temporarily vary the bit length limit.
   *
   * Implementing as a macro eliminates the creation of a downward function object every time this
   * is called.
   *
   * ISSUE: this macro really wants to use a self reference to `this`. But when a macro is expanded
   * the object that `this` represents changes. Until a better way to do this comes about, we have to pass
   * the `this` object to the `self` argument, which makes calls look like:
   *     dis.withBitLengthLimit(dis, newLimit){... body ...}
   * That looks redundant, and it is, but it's more important to get the allocation of this downward function
   * object out of inner loops.
   */
  def withBitLengthLimitMacro(c: Context)(self: c.Tree, lengthLimitInBits: c.Tree)(body: c.Tree) = {

    import c.universe._

    q"""{
    import edu.illinois.ncsa.daffodil.util.MaybeULong

    val ___dStream = $self
    val ___newLengthLimit = $lengthLimitInBits
    val ___savedLengthLimit = ___dStream.bitLimit0b

    if (!___dStream.setBitLimit0b(MaybeULong(___dStream.bitPos0b + ___newLengthLimit))) false
    else {
      try {
        $body
      } finally {
        ___dStream.resetBitLimit0b(___savedLengthLimit)
      }
      true
    }
    }"""
}

Upvotes: 3

Views: 578

Answers (1)

Travis Brown
Travis Brown

Reputation: 139038

The prefix method on Context provides access to the expression that the macro method is called on, which should allow you to accomplish what you're trying to do. Here's a quick example of how you can use it:

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

class Foo(val i: Int) {
  def bar: String = macro FooMacros.barImpl
}

object FooMacros {
  def barImpl(c: Context): c.Tree = {
    import c.universe._

    val self = c.prefix

    q"_root_.scala.List.fill($self.i + $self.i)(${ self.tree.toString }).mkString"
  }
}

And then:

scala> val foo = new Foo(3)
foo: Foo = Foo@6fd7c13e

scala> foo.bar
res0: String = foofoofoofoofoofoo

Note that there are some issues you need to be aware of. prefix gives you the expression, which may not be a variable name:

scala> new Foo(2).bar
res1: String = new Foo(2)new Foo(2)new Foo(2)new Foo(2)

This means that if the expression has side effects, you have to take care not to include it in the result tree more than once (assuming you don't want them to happen multiple times):

scala> new Qux(1).bar
hey
hey
res2: String = new Qux(1)new Qux(1)

Here the constructor is called twice since we include the prefix expression in the macro's result twice. You can avoid this by defining a temporary variable in the macro:

object FooMacros {
  def barImpl(c: Context): c.Tree = {
    import c.universe._

    val tmp = TermName(c.freshName)
    val self = c.prefix

    q"""
    {
      val $tmp = $self

      _root_.scala.List.fill($tmp.i + $tmp.i)(${ self.tree.toString }).mkString
    }
    """
  }
}

And then:

scala> class Qux(i: Int) extends Foo(i) { println("hey") }
defined class Qux

scala> new Qux(1).bar
hey
res3: String = new Qux(1)new Qux(1)

Note that this approach (using freshName) is a lot better than just prefixing local variables in the macro with a bunch of underscores, which can cause problems if you include an expression that happens to contain a variable with the same name.

(Update about that last paragraph: actually I don't remember for sure if you can get yourself into problems with local variable names shadowing names that might be used in included trees. I avoid it myself, but I can't manufacture an example of it causing problems at the moment, so it might be fine.)

Upvotes: 4

Related Questions