Search code examples
scalaimplicitscala-macrostype-level-computation

Searching for implicit inside a reify call (scala macro)


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.


Solution

  • 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

    Get an scala.MatchError: f (of class scala.reflect.internal.Trees$Ident) when providing a lambda assigned to a val

    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.