0

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.

Merlin
  • 224
  • 1
  • 11

1 Answers1

1

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Using your code I get the following error on first line : ```java.lang.IndexOutOfBoundsException: 0``` After a lot of reading it seems that my error was using `c.prefix`. I was thinking of it as `this` or `self` of the `Delayed` instance. It seems to be a bit different and depends on the call site of the macro. Do you know how to point to the 'this' of `Delayed` anytime? (And thank's for the help of course) – Merlin Apr 23 '19 at 14:04
  • @Merlin You provide not enough information to reproduce `IndexOutOfBoundsException`. – Dmytro Mitin Apr 23 '19 at 14:15
  • @Merlin If you add `println(c.prefix.tree)` to the macro, then compilation of `object App { trait Kind[_[_]]; trait F[_]; implicit val kindF: Kind[F] = new Kind[F] {}; new Delayed[Kind]("some existing file", 0).find[F] }` prints `Warning:scalac: new App.Delayed[App.Kind]("some existing file", 0)`. So `c.prefix` is like `this`. – Dmytro Mitin Apr 23 '19 at 14:19
  • It seems to be when storing the `delayed` instance and passing it around. It now this is not enough information, I'll keep on searching after work. – Merlin Apr 23 '19 at 14:52
  • If I change you example a bit : `val tmp = new Delayed[Kind](...); tmp.find[F]` In this case it will print `YourApp.type.tmp` as prefix. Which obviously is not what I meant. – Merlin Apr 24 '19 at 07:52
  • To be fair I think it can't work because eval need to be able to de-reference the actual variable, therefore accessing it's type. Because the type is in another sbt project, it can't do it. – Merlin Apr 24 '19 at 08:07
  • @Merlin Surely, runtime value can't be accessed at compile time. – Dmytro Mitin Apr 24 '19 at 08:34
  • All the pieces of information are available at compile time, but that's true that I store them in runtime values. How would you avoid that ? Also, what you are saying seems strange. Isn't accessing runtime values what c.prefix allow? – Merlin Apr 24 '19 at 08:38
  • @Merlin In `val x = 10`, `somemacro(x)` value `10` is unaccessible by the macro i.e. unaccessible at compile time. `eval` can help with `somemacro(5+5)` – Dmytro Mitin Apr 24 '19 at 08:48
  • @Merlin *"Also, what you are saying seems strange. Isn't accessing runtime values what `c.prefix` allow?"* `c.prefix` is a Tree `this`, not a runtime-value `this`. – Dmytro Mitin Oct 22 '22 at 20:20
  • @Merlin Please see the update with `Traverser`. – Dmytro Mitin Oct 23 '22 at 03:47
  • Will look into it, not working on that right now but will try to find some time. Thank's a lot :) – Merlin Jul 05 '23 at 10:20