Can you implement dsinfo in Scala 3? (Can Scala 3 macros get info about their context?)

Viewed 53

The dsinfo library lets you access the names of values from the context of where a function is written using Scala 2 macros. The example they give is that if you have something like

val name = myFunction(x, y)

myFunction will actually be passed the name of its val in addition to the other arguments, i.e., myFunction("name", x, y).

This is very useful for DSLs where you'd like named values for error reporting or other kinds of encoding. The only other option seems to explicitly pass the name as a String, which can lead to unintentional mismatches.

Is this possible with Scala 3 macros, and if so, how do you "climb up" the tree at the macro's use location to find its id?

1 Answers

In Scala 3 there is no c.macroApplication. Only Position.ofMacroExpansion instead of a tree. But we can analyze Symbol.spliceOwner.maybeOwner. I presume that scalacOptions += "-Yretain-trees" is switched on.

import scala.annotation.experimental
import scala.quoted.*

object Macro {
  inline def makeCallWithName[T](inline methodName: String): T = 
    ${makeCallWithNameImpl[T]('methodName)}

  @experimental
  def makeCallWithNameImpl[T](methodName: Expr[String])(using Quotes, Type[T]): Expr[T] = {
    import quotes.reflect.*
    println(Position.ofMacroExpansion.sourceCode)//Some(twoargs(1, "one"))

    val methodNameStr = methodName.valueOrAbort
    val strs = methodNameStr.split('.')
    val moduleName = strs.init.mkString(".")
    val moduleSymbol = Symbol.requiredModule(moduleName)

    val shortMethodName = strs.last
    val ident = Ident(TermRef(moduleSymbol.termRef, shortMethodName))

    val (ownerName, ownerRhs) = Symbol.spliceOwner.maybeOwner.tree match {
      case ValDef(name, tpt, Some(rhs)) => (name, rhs)
      case DefDef(name, paramss, tpt, Some(rhs)) => (name, rhs)
      case t => report.errorAndAbort(s"can't find RHS of ${t.show}")
    }

    val treeAccumulator = new TreeAccumulator[Option[Tree]] {
      override def foldTree(acc: Option[Tree], tree: Tree)(owner: Symbol): Option[Tree] = tree match {
        case Apply(fun, args) if fun.symbol.fullName == "App$.twoargs" =>
          Some(Apply(ident, Literal(StringConstant(ownerName)) :: args))
        case _ => foldOverTree(acc, tree)(owner)
      }
    }
    treeAccumulator.foldTree(None, ownerRhs)(ownerRhs.symbol)
      .getOrElse(report.errorAndAbort(s"can't find twoargs in RHS: ${ownerRhs.show}"))
      .asExprOf[T]
  }
}

Usage:

package mypackage
case class TwoArgs(name : String, i : Int, s : String)
import mypackage.TwoArgs

object App {
  inline def twoargs(i: Int, s: String) = 
    Macro.makeCallWithName[TwoArgs]("mypackage.TwoArgs.apply")

  def x() = twoargs(1, "one") // TwoArgs("x", 1, "one")

  def aMethod() = {
    val y = twoargs(2, "two") // TwoArgs("y", 2, "two")
  }

  val z = Some(twoargs(3, "three")) // Some(TwoArgs("z", 3, "three"))
}

dsinfo also handles the name twoargs at call site (as template $macro) but I didn't implement this. I guess the name (if necessary) can be obtained from Position.ofMacroExpansion.sourceCode.

Related