2

I just started studying scala compile-time reflection, and I got introduced to quasiquotes by the Scala official guides.

One concept I'm still struggling with is how am I supposed to work with quasiquotes (or reify, for that matter) if I want to generate the AST for an already defined object. Suppose I have an Object:

object MyObject {
  def method1() = "m1"
}

In order to get a tree, I know I can do

q"""{object MyObject {
  def method1() = "m1"
}}
"""

Doing this, however, prevents me from having the object actually defined in my scope (and I also need to define it entirely inside a String, throwing all code safety out of the window).

What I'd like to do to get that tree is something like this:

object MyObject {
  def method1() = "m1"
}

q"$MyObject" // or q"{MyObject}", I still don't fully understand the table on the Scala guide

I want to define the object, and, afterwards, use that definition to perform some checks over it (and throw some exception in compile-time, if need be), using a macro. To use a macro, I'll need to tree (or, at least, the expression), as far as I understood.

I already know how to do the checks I want using Scala reflection in run-time, but I thought using ASTs could be a good idea (and, on the process, I would learn something). I'm getting the feeling that I'm misunderstanding some basic concept on how to use ASTs, though - it seems like one can generate ASTs based on code declared on the call site only. I'm confused.

What am I misunderstanding here?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Lucas Lima
  • 832
  • 11
  • 23
  • See [macro annotations](https://docs.scala-lang.org/overviews/macros/annotations.html) – user Jun 18 '20 at 21:24
  • I've read the entire text, understood half of it, and learned how to apply none of it. :( – Lucas Lima Jun 18 '20 at 22:08
  • Yeah, I've never been able to get macro annotations to work either. What kind of checks are these? Do they involve method signatures and stuff and are they always known at compile time? – user Jun 18 '20 at 22:10
  • They only involve method signatures. Essentially, I have an object, and I want to ensure of its methods have a specific signature, although I don't know how many methods will there be. Of course I could declare a trait and list every single method, but, when adding a new one, I'd need to declare the method, and register it on the trait, which is lame. Currently, I'm using runtime reflection to check the types, but the error will only pop up when running tests. I wanted to improve the experience of the developer by throwing the error right away, on the compilation. – Lucas Lima Jun 18 '20 at 22:28
  • @LucasLima Your question sounds like [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). It would be better if you create a question with your actual problem (object, checks etc.) with [MCVE](https://stackoverflow.com/help/minimal-reproducible-example). – Dmytro Mitin Jun 18 '20 at 22:53
  • @LucasLima I guess your macro can look like `def foo[A](a: A) = macro impl[A]` or `def foo[A] = macro impl[A]` so you can call it like `foo(MyObject)` or `foo[MyObject.type]` and inside `def impl[A: c.WeakTypeTag](c: blackbox.Context)...` you have access to `weakTypeOf[A]`, then its symbol. Having symbol you can have signatures of methods etc. – Dmytro Mitin Jun 18 '20 at 23:05
  • @DmytroMitin I did not ask for the problem specifically because I already have an answer to that using runtime reflection. I already opened a question, and some other good soul helped me to figure it out. Now, with your answer, I think I have all I need to solve the problem - and I also understood how things work properly. Thanks. – Lucas Lima Jun 18 '20 at 23:30
  • @LucasLima Actually, in some sense there is a way to "insert" an object into a quasiquote. This is serialization/deserialization objects between stages. See update. – Dmytro Mitin Nov 02 '22 at 14:09

1 Answers1

2

A quasiquote

q"""{object MyObject {
  def method1() = "m1"
}}
"""

or

reify{
  object MyObject {
    def method1() = "m1"
  }
}.tree

are just ways to write a tree

Block(
  List(
    ModuleDef(Modifiers(), TermName("MyObject"), 
      Template(
        List(Select(Ident(scala), TypeName("AnyRef"))), 
        noSelfType, 
        List(
          DefDef(Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree(), 
            Block(List(pendingSuperCall), Literal(Constant(())))
          ), 
          DefDef(Modifiers(), TermName("method1"), List(), List(List()), TypeTree(), 
            Literal(Constant("m1"))
          )
        )
      )
    )
  ),
  Literal(Constant(()))
)

The same can be obtained with context.parse (compile-time) / toolBox.parse (runtime) from ordinary String

val str: String = 
  """object MyObject {
    |  def method1() = "m1"
    |}""".stripMargin

toolBox.parse(str)

There is compile time of macros and runtime of macros. There is compile time of main code and its runtime. Runtime of macros is compile time of main code.

MyObject in

object MyObject {
  def method1() = "m1"
}

and MyObject in

q"""{object MyObject {
  def method1() = "m1"
}}
"""

exist in different contexts. The former exists in the current context, the latter exists in the context of macro's call site.

You can insert (splice) a tree into a tree. You can not insert actual object into a tree. If you have actual object (compiled tree) it's too late to insert it into a tree.

When you see something being inserted into a tree, this means that "something" is just a compact way to write a tree i.e. an instance of type class Liftable

object MyObject {
  def method1() = "m1"
}

implicit val myObjectLiftable: Liftable[MyObject.type] = new Liftable[MyObject.type] {
  override def apply(value: MyObject.type): Tree =
    q"""
      object MyObject {
        def method1() = "m1"
      }"""
}
  
q"""
   class SomeClass {
     $MyObject
   }"""

I guess your macro can look like

def foo[A](a: A) = macro impl[A]

or

def foo[A] = macro impl[A]

so you can call it like foo(MyObject) or foo[MyObject.type] and inside

def impl[A: c.WeakTypeTag](c: blackbox.Context)...

you have access to weakTypeOf[A], then its symbol. Having symbol you can have signatures of methods etc.


Actually, in some sense there is a way to "insert" an object into a quasiquote. This is serialization/deserialization objects between stages

import java.io.FileOutputStream
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
import scala.util.Using
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Output

object MyObject {
  def method1() = "m1"
}

def myMacro(): String = macro myMacroImpl

def myMacroImpl(c: blackbox.Context)(): c.Tree = {
  import c.universe._

  val kryo = new Kryo
  kryo.register(classOf[MyObject.type])
  Using(new Output(new FileOutputStream("file"))) { output =>
    kryo.writeClassAndObject(output, MyObject)
  }

  val kryoPrefix = q"_root_.com.esotericsoftware.kryo"
  q"""
    val kryo = new $kryoPrefix.Kryo
    kryo.register(classOf[MyObject.type])
    _root_.scala.util.Using(new $kryoPrefix.io.Input(new _root_.java.io.FileInputStream("file"))) { input =>
      kryo.readClassAndObject(input).asInstanceOf[MyObject.type]
    }.get.method1()
  """
}
myMacro()

sbt clean compile run prints m1 (file file must exist, this doesn't work in IntelliJ).

scalaVersion := "2.13.10"
libraryDependencies += "com.esotericsoftware" % "kryo" % "5.3.0"
//libraryDependencies += "com.esotericsoftware.kryo" % "kryo5" % "5.3.0"

Similarly you can use @tribbloid's code in splain

case class MyClass(i: Int, s: String)

import scala.language.experimental.macros
import scala.reflect.macros.whitebox
import splain.test.AutoLift.SerializingLift

def myMacro(): Any = macro Macros.myMacroImpl

class Macros(val c: whitebox.Context) extends SerializingLift.Mixin {
  import c.universe._

  def myMacroImpl(): c.Tree = {
    q"""
      ${MyClass(1, "a")}
    """
  }
}
val res = Macro.myMacro() //scalac: splain.test.AutoLift.SerializingLift.fromPreviousStage[mypackage.MyClass]("rO0ABXNyAA5hcHAxODEuTXlDbGFzczOlcqPGO50dAgACSQABaUwAAXN0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAF0AAFh")
res: MyClass // since the macro is whitebox, it can return more precise type than declared (Any)
println(res) //MyClass(1,a)
scalaVersion := "2.13.10"
libraryDependencies += "io.tryp" % "splain" % "1.0.1" cross CrossVersion.full

Unfortunately this doesn't work with (case) objects because of a bug.

In multi-stage compilation, should we use a standard serialisation method to ship objects through stages?

https://contributors.scala-lang.org/t/in-multi-stage-compilation-should-we-use-a-standard-serialisation-method-to-ship-objects-through-stages/5699

https://github.com/EsotericSoftware/kryo

https://com-lihaoyi.github.io/upickle/#uPack


One more technique to define a macro if a value is from the next stage (and avoid cross-stage evaluation) is to construct a tree of function and then apply this function after macro expansion

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

case class MyClass(i: Int, s: String)

def myMacro(): Any = macro myMacroImpl

def myMacroImpl(c: whitebox.Context)(): c.Tree = {
  import c.universe._
  q"""
    def foo[A](a: A): Unit = println("foo: a=" + a)

    foo(_: ${typeOf[MyClass]})
  """
}
val f = myMacro()
//scalac: {
//  def foo[A](a: A): Unit = println("foo: a=".$plus(a));
//  ((x$1: MyClass) => foo((x$1: MyClass)))
//}

f: (MyClass => Unit) // checking the type

f(MyClass(1, "a"))
//foo: a=MyClass(1,a)

Scala 2.13: Case class with extendable variable attributes?

Invoke a template Scala function with a type stored as wild card classTag?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • `There is compile time of macros and runtime of macros. There is compile time of main code and its runtime. Runtime of macros is compile time of main code.` This is exactly what was missing in my comprehension. Your answer fully quenched my doubts. Thank you. As I said in a comment above, I have the answer fully sorted out using runtime reflection, but, now that you explained it, if I can use runtime reflection on compiletime via macros, that solves everything, all pieces fall together. ` I just need to tie the two ends together. Thanks! – Lucas Lima Jun 18 '20 at 23:29