How to get the runtime value of parameter passed to a Scala macro?
Asked Answered
R

1

6

I have an ostensibly simple macro problem that I’ve been banging my head against for a few hours, with no luck. Perhaps someone with more experience can help.

I have the following macro:

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

object MacroObject {
  def run(s: String): Unit =
    macro runImpl

  def runImpl(c: Context)(s: c.Tree): c.Tree = {
    import c.universe._
    println(s)    // <-- I need the macro to know the value of s at compile time
    q"()"
  }
}

The problem is this: I’d like the macro to know the value s that is passed to it — not an AST of s, but the value of s itself. Specifically, I’d like it to have this behavior:

def runTheMacro(str: String): Unit = MacroObject.run(str)

final val HardCodedString1 = "Hello, world!"
runTheMacro(HardCodedString1)    // the macro should print "Hello, world!"
                                 // to the console during macro expansion

final val HardCodedString2 = "So long!"
runTheMacro(HardCodedString2)    // the macro should print "So long!"
                                 // to the console during macro expansion

It is guaranteed that the only strings that will be passed to runTheMacro are hard-coded constant values (i.e., known at compile-time).

Is this possible, and how does one do this?

--

Edit: There are also the following constraints:

  1. It must be a blackbox macro.
  2. The macro signature must use c.Trees, not c.Expr[_]s (legacy code; can’t change that part)
  3. I do have a toolbox within the macro at my disposal if needed:
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
private val toolbox = currentMirror.mkToolBox()

/** Evaluate the given code fragment at compile time. */
private def eval[A](code: String): A = {
  import scala.reflect.runtime.{universe => u}
  val uTree: u.Tree = toolbox.parse(code)
  toolbox.eval(uTree).asInstanceOf[A]
}
Ripley answered 18/12, 2019 at 7:39 Comment(3)
Thanks Andriy. I’ve updated my question to clarify the constraints.Ripley
Andriy — I tried that trick in the link you sent, and unfortunately it generates an exception during macro expansion: java.lang.IllegalArgumentException: Could not find proxy for str: String in List(value str, method run, object cmd2, package $sess, package ammonite, package <root>) (currentOwner= method wrapper )Ripley
I think you should be able to pattern-match the s to be Literal(Constant(s: String)), and if it doesn't match, just call c.abortGyrostabilizer
S
1

Your eval is runtime reflection's eval, compile-time macro's eval would be c.eval.

"Hello, world!" in

final val HardCodedString1 = "Hello, world!"
runTheMacro(HardCodedString1) 

is a runtime value of HardCodedString1.

You can't have access to runtime value at compile time.

At compile time the tree of string HardCodedString1 just doesn't know anything about right hand side of the val tree.

Scala: what can code in Context.eval reference?

If you really need to use a runtime value inside the tree of your program you have to postpone its compilation till runtime

import scala.reflect.runtime.currentMirror
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox

object MacroObject {
  val toolbox = currentMirror.mkToolBox()

  def run(s: String): Unit = {
    toolbox.eval(q"""
      println($s)
      ()
    """)
  }
}

runTheMacro(HardCodedString1)//Hello, world!
runTheMacro(HardCodedString2)//So long!

Alternatively at compile time you can somehow find the tree of enclosing class and look inside it for the val tree and take its right hand side

def runImpl(c: blackbox.Context)(s: c.Tree): c.Tree = {
  import c.universe._

  var rhs: Tree = null

  val traverser = new Traverser {
    override def traverse(tree: Tree): Unit = {
      tree match {
        case q"$mods val $tname: $tpt = $expr" if tname == TermName("HardCodedString1") =>
          rhs = expr
        case _ => ()
      }
      super.traverse(tree)
    }
  }

  traverser.traverse(c.enclosingClass) // deprecated

  val rhsStr =
    if (rhs != null) c.eval[String](c.Expr(c.untypecheck(rhs.duplicate)))
    else c.abort(c.enclosingPosition, "no val HardCodedString1 defined")

  println(rhsStr)

  q"()"
}

runTheMacro(HardCodedString1)//Warning:scalac: Hello, world!

Or for all such variables

def runImpl(c: blackbox.Context)(s: c.Tree): c.Tree = {
  import c.universe._

  val sEvaluated =
    try {
      c.eval[String](c.Expr(c.untypecheck(s.duplicate)))
    } catch {
      case e: IllegalArgumentException if e.getMessage.startsWith("Could not find proxy") =>
        s match {
          case q"$sName" =>
            var rhs: Tree = null

            val traverser = new Traverser {
              override def traverse(tree: Tree): Unit = {
                tree match {
                  case q"$mods val $tname: $tpt = $expr" if tname == sName =>
                    rhs = expr
                  case _ => ()
                }
                super.traverse(tree)
              }
            }

            traverser.traverse(c.enclosingClass)

            if (rhs != null) c.eval[String](c.Expr(c.untypecheck(rhs.duplicate)))
            else c.abort(c.enclosingPosition, s"no val $sName defined")

          case _ => c.abort(c.enclosingPosition, s"unsupported tree $s")
        }

    }

  println(sEvaluated)

  q"()"
}

MacroObject.run(HardCodedString1) //Warning:scalac: Hello, world!
MacroObject.run(HardCodedString2) //Warning:scalac: So long!

runTheMacro will not work in this case: Error: no val str defined. To make it work you can make it a macro too

def runTheMacro(str: String): Unit = macro runTheMacroImpl

def runTheMacroImpl(c: blackbox.Context)(str: c.Tree): c.Tree = {
  import c.universe._
  q"MacroObject.run($str)"
}

runTheMacro(HardCodedString1) //Warning:scalac: Hello, world!
runTheMacro(HardCodedString2) //Warning:scalac: So long!
Schrimsher answered 18/12, 2019 at 21:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.