I need to search for an implicit value at a given position. I retained the position from a previous macro call in a class, like so :
class Delayed[+Kind[_[_]]](val sourceFilePath: String, val callSitePoint: Int) {
def find[F[_]]: Kind[F] = macro Impl.find[Kind, F]
}
The previous macro is very simple :
def build[Kind[_[_]]](c: blackbox.Context): c.Expr[Delayed[Kind]] = {
import c.universe._
c.Expr(
q"""
new Delayed(${c.enclosingPosition.point}, ${c.enclosingPosition.source.path})
"""
)
}
With this I have the position, all I need to do is to launch the implicit search right ?
def find[Kind[_[_]], F[_]](c: blackbox.Context)(implicit kindTag: c.WeakTypeTag[Kind[F]], fTag: c.WeakTypeTag[F[_]]): c.Expr[Kind[F]] = {
import c.universe._
reify {
val self = c.prefix.splice.asInstanceOf[Delayed[Kind]]
val sourceFile = AbstractFile.getFile(self.sourceFilePath)
val batchSourceFile = new BatchSourceFile(sourceFile, sourceFile.toCharArray)
val implicitSearchPosition = new OffsetPosition(batchSourceFile, self.callSitePoint).asInstanceOf[c.Position]
c.Expr[Kind[F]](c.inferImplicitValue(
appliedType(kindTag.tpe.typeConstructor, fTag.tpe.typeConstructor),
pos = implicitSearchPosition
)).splice
}
}
I get the position using reify/splice calls and then apply inferImplicitValue
. But the compiler complains about the last splice on the implicit value :
the splice cannot be resolved statically,
which means there is a cross-stage evaluation involved
It asks me to add the compiler jar as dependencies, but by doing so I only get another error :
Macro expansion contains free term variable c defined by find in Delayed.scala
I understand that reify is, conceptually, in the world of values. What I don't understand is that the implicit search should be resolved before the macro-generated code is written to my source code. That the only way I can think of for the implicit search to work in macro contexts.
Where I am wrong ? I do understand the compiler messages, but to me, it makes no sense in this particular context. Maybe I don't get how inferImplicitValue
works.
Try Context#eval(expr)
def find[Kind[_[_]], F[_]](c: blackbox.Context)(implicit kindTag: c.WeakTypeTag[Kind[F]], fTag: c.WeakTypeTag[F[_]]): c.Expr[Kind[F]] = {
import c.universe._
val self = c.eval(c.Expr[Delayed[Kind]](c.untypecheck(c.prefix.tree.duplicate)))
val sourceFile = AbstractFile.getFile(self.sourceFilePath)
val batchSourceFile = new BatchSourceFile(sourceFile, sourceFile.toCharArray)
val implicitSearchPosition = new OffsetPosition(batchSourceFile, self.callSitePoint).asInstanceOf[c.Position]
c.Expr[Kind[F]](c.inferImplicitValue(
appliedType(kindTag.tpe.typeConstructor, fTag.tpe.typeConstructor),
pos = implicitSearchPosition
))
}
Alternatively you can try to find the right hand side of the prefix definition before evaluating it:
macros/src/main/scala/Delayed.scala
import scala.language.experimental.macros
import scala.reflect.internal.util.{BatchSourceFile, OffsetPosition}
import scala.reflect.io.AbstractFile
import scala.reflect.macros.whitebox
class Delayed[+Kind[_[_]]](val sourceFilePath: String, val callSitePoint: Int) {
def find[F[_]]: Kind[F] = macro Impl.find[Kind, F]
}
object Delayed {
def build[Kind[_[_]]]: Delayed[Kind] = macro Impl.build[Kind]
}
class Impl(val c: whitebox.Context) {
import c.universe._
def build[Kind[_[_]]](implicit kindTag: c.WeakTypeTag[Kind[Any]/*[F] forSome {type F[_]}*/]): c.Expr[Delayed[Kind]] = {
c.Expr[Delayed[Kind]](
q"""
new Delayed[${kindTag.tpe.typeConstructor}](${c.enclosingPosition.source.path}, ${c.enclosingPosition.point})
"""
)
}
def find[Kind[_[_]], F[_]](implicit kindTag: c.WeakTypeTag[Kind[Any]], fTag: c.WeakTypeTag[F[_]]): c.Expr[Kind[F]] = {
val prefix = c.prefix.tree
val prefixSymbol = prefix.symbol
def eval[A: WeakTypeTag](tree: Tree): Either[Throwable, A] = {
// import org.scalamacros.resetallattrs._ // libraryDependencies += "org.scalamacros" %% "resetallattrs" % "1.0.0" // https://github.com/scalamacros/resetallattrs
// util.Try(c.eval(c.Expr[A](c.resetAllAttrs(tree.duplicate)))).toEither
util.Try(c.eval(c.Expr[A](c.untypecheck(c.typecheck(tree/*.duplicate*/))))).toEither // see (*) below
}
val self: Delayed[Kind] = eval[Delayed[Kind]](prefix).orElse {
var rhs: Either[Throwable, Tree] = Left(new RuntimeException(s"can't find RHS of definition of $prefix"))
val traverser = new Traverser {
override def traverse(tree: Tree): Unit = {
tree match {
case q"$_ val $_: $_ = $expr"
if tree.symbol == prefixSymbol ||
(tree.symbol.isTerm && tree.symbol.asTerm.getter == prefixSymbol) =>
rhs = Right(expr)
case _ =>
super.traverse(tree)
}
}
}
c.enclosingRun.units.foreach(unit => traverser.traverse(unit.body))
rhs.flatMap(eval[Delayed[Kind]])
}.fold(err => c.abort(c.enclosingPosition, s"can't find or eval self because: $err"), identity)
val sourceFile = AbstractFile.getFile(self.sourceFilePath)
val batchSourceFile = new BatchSourceFile(sourceFile, sourceFile.toCharArray)
val implicitSearchPosition = new OffsetPosition(batchSourceFile, self.callSitePoint).asInstanceOf[c.Position]
c.Expr[Kind[F]](c.inferImplicitValue(
appliedType(kindTag.tpe.typeConstructor, fTag.tpe.typeConstructor),
silent = false,
pos = implicitSearchPosition
))
}
}
But I suspect that c.inferImplicitValue
with specified position works not as you expected. I guess the position is used for error messages, not for managing scopes of implicit resolution. If you want to play with priority of implicit resolution in such way you could use compiler internals (1 2 3 4 5).
macros/src/main/scala/Functor.scala
trait Functor[F[_]]
core/src/main/scala/App.scala
object A {
implicit val f: Functor[List] = new Functor[List] {}
val d: Delayed[Functor] = Delayed.build[Functor]
}
object App {
implicit val f1: Functor[List] = new Functor[List] {}
A.d.find[List] // scalac: App.this.f1 // not A.f
}
Scala 2.13.10
Def Macro, pass parameter from a value
Creating a method definition tree from a method symbol and a body
Scala macro how to convert a MethodSymbol to DefDef with parameter default values?
How to get the runtime value of parameter passed to a Scala macro?
Scala: what can code in Context.eval reference?
How to get the body of variable initialisation from outer scope in Scala 3 macros? (Scala 3)
(*) .duplicate
is actually not necessary: https://github.com/readren/json-facile/pull/1#issuecomment-733886784
In principle, a value calculated at this stage can be persisted for the next stage via serialization/deserialization (permitting a kind of cross-stage evaluation)
How to use quasiquotes with previously defined object
But it's hardly possible to serialize c: Context
.