Reputation: 1498
It seems that the input of Context.eval
can reference only values from different compilation unit:
// project 1
object Z {
val foo = "WOOF"
def impl(c: Context)(x: c.Expr[String]) = {
val x1 = c.Expr[String](c.untypecheck(x.tree.duplicate))
println(s"compile-time value is: ${c.eval(x1)}")
x
}
def test(x: String) = macro impl
}
// project 2
object Y {
val foo = "GOOF"
val boo = Z.test(Z.foo)
}
println(Y.boo)
prints out "WOOF"
, but if I replace boo with val boo = Z.test(Y.foo)
, I get the following compilation error:
Error:(32, 29) exception during macro expansion:
java.lang.ClassNotFoundException: Y$
at scala.reflect.internal.util.AbstractFileClassLoader.findClass(AbstractFileClassLoader.scala:72)
...
Is there any way around this problem? I know that the queries defined with quill.io can reference methods from the same scope, but I wasn't able to find the trick they use to allow it.
Upvotes: 3
Views: 333
Reputation: 51683
Context#eval
can't evaluate runtime values. It's written in its scaladoc: https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/macros/Evals.scala#L61-L67
Let's modify your macro
def impl(c: blackbox.Context)(x: c.Expr[String]): c.Expr[String] = {
import c.universe._
println(s"input: ${showRaw(x.tree)}") // added
val x1 = c.Expr[String](c.untypecheck(x.tree.duplicate))
println(s"compile-time value is: ${c.eval(x1)}")
x
}
Then we'll have
object App {
/*class*/ object Y {
val foo = "GOOF"
val boo = Z.test(Z.foo)//Warning:scalac: input: Select(Select(Ident(Macros), Macros.Z), TermName("foo"))
//Warning:scalac: compile-time value is: WOOF
// val boo1 = Z.test(Y.foo)//Warning:scalac: input: Select(Select(This(TypeName("App")), App.Y), TermName("foo"))
//Error: exception during macro expansion:
// java.lang.ClassNotFoundException: App$Y$
// val boo2 = Z.test((new Y).foo)//Warning:scalac: input: Select(Apply(Select(New(Select(This(TypeName("App")), App.Y)), termNames.CONSTRUCTOR), List()), TermName("foo"))
//Error: exception during macro expansion:
// java.lang.ClassNotFoundException: App$Y
// val boo3 = Z.test(foo) //Warning:scalac: input: Select(This(TypeName("Y")), TermName("foo"))
//Error: exception during macro expansion:
// scala.tools.reflect.ToolBoxError: reflective compilation has failed:
// Internal error: unable to find the outer accessor symbol of object __wrapper$1$fd3cb1297ce8421e809ee5e821c2f708
// or
//Error: exception during macro expansion:
// java.lang.ClassNotFoundException: App$Y$
val boo4 = Z.test("abc")//Warning:scalac: input: Literal(Constant("abc"))
//Warning:scalac: compile-time value is: abc
val boo5 = Z.test("abc" + "DEF")//Warning:scalac: input: Literal(Constant("abcDEF"))
//Warning:scalac: compile-time value is: abcDEF
}
}
Tree This
means that it represents a runtime value. Just ClassNotFoundException
sometimes happens faster than ToolBoxError
. You subproject with macros project 1
doesn't depend on subproject project 2
so during compilation of macros Y
is not found.
Difference between Z.foo
and foo
(aka Y.foo
) is that foo
is actually this.foo
(compiler doesn't care here if Y
is a class or object) and can be overriden in subclasses.
Quill doesn't use eval
. It parses a tree into its own AST if it can or leave Dynamic
if it can't (i.e. if the tree corresponds to runtime value). And then it works with these two case differently: either during macros expansion with QueryMeta
or during compile time + runtime with Decoder
So the workaround is to work with runtime values at runtime.
def impl(c: blackbox.Context)(x: c.Expr[String]): c.Expr[String] = {
import c.universe._
println(s"input: ${showRaw(x.tree)}")
try {
val x1 = c.Expr[String](c.untypecheck(x.tree.duplicate))
val x2 = c.eval(x1)
println(s"compile-time value is: $x2")
c.Expr[String](q"$x2")
} catch {
case ex: Throwable =>
println(ex.getMessage)
x
}
}
This is similar to https://github.com/getquill/quill/blob/master/quill-core/src/main/scala/io/getquill/context/ContextMacro.scala#L66-L68
Upvotes: 5