Search code examples
scalamacrosscala-macros

How to use a `universe.Tree` created and type-checked in one Scala macro execution, in another macro execution?


In the JSON library jsfacile I was able to automatically derive type-classes for types with recursive type references thanx to a buffer where outer executions of a macro store instances of universe.Tree which are used later by inner executions of the same or other macro. This works fine as long as the universe.Tree instance was not type-checked (with the Typers.typecheck method).

The problem is that this forces to type-check the same Tree instance more than one time: one time in the macro execution that created it (after having stored it in the buffer); and more times in each inner macro execution that needs it.

The intention of this question is to find out a way to share the Tree instance between macro executions after it was type-checked; in order to improve compilation speed.

I tried to wrap the type-checked Tree into a universe.Expr[Unit] and migrate it to the mirror of the macro execution that uses it by means of the Expr.in[U <: Universe](otherMirror: Mirror[U]): U # Expr[T] method. But it fails with an internal error:

Internal error: unable to find the outer accessor symbol of class <Name of the class where the macro is expanded>

Any idea?


Solution

  • Generally, typechecking a tree manually and sharing the typed tree among different contexts is a bad idea. See the following example:

    import scala.language.experimental.macros
    import scala.reflect.macros.whitebox
    import scala.collection.mutable
    
    object Macros {
      val mtcCache = mutable.Map[whitebox.Context#Type, whitebox.Context#Tree]()
    
      trait MyTypeclass[A]
    
      object MyTypeclass {
        implicit def materialize[A]: MyTypeclass[A] = macro materializeImpl[A]
    
        def materializeImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
          import c.universe._
    
          val typA = weakTypeOf[A]
    
          if (!mtcCache.contains(typA)) 
            mtcCache += (typA -> c.typecheck(q"new MyTypeclass[$typA] {}"))
    
          mtcCache(typA).asInstanceOf[Tree]
        }
      }
    }
    
    import Macros.MyTypeclass
    
    object App { // Internal error: unable to find the outer accessor symbol of object App
    
      class A { // Internal error: unable to find the outer accessor symbol of class A
    
        class B { // Internal error: unable to find the outer accessor symbol of class A
    
          class C {
            implicitly[MyTypeclass[Int]] // new MyTypeclass[Int] {} is created and typechecked here
          }
    
          implicitly[MyTypeclass[Int]] // cached typed instance is inserted here, this is the reason of above error
        }
    
        implicitly[MyTypeclass[Int]] // cached typed instance is inserted here, this is the reason of above error
      }
    
      implicitly[MyTypeclass[Int]] // cached typed instance is inserted here, this is the reason of above error
    }
    

    Scala 2.13.3.

    With implicitly we put in some places trees with incorrect symbol owner chain.

    If you make A, B, C objects then errors disappear (so whether this prevents compilation depends on a luck).

    Also if you remove c.typecheck then errors disappear.

    Also if we return c.untypecheck(mtcCache(typA).asInstanceOf[Tree]) instead of mtcCache(typA).asInstanceOf[Tree] then errors disappear. But sometimes c.typecheck + c.untypecheck can damage a tree.

    So you can try to put both untyped and typed versions of a tree to the cache if you need both but return the untyped one

    type CTree = whitebox.Context#Tree
    val mtcCache = mutable.Map[whitebox.Context#Type, (CTree, CTree)]()
    
    trait MyTypeclass[A]
    
    object MyTypeclass {
      implicit def materialize[A]: MyTypeclass[A] = macro materializeImpl[A]
    
      def materializeImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
    
        val typA = weakTypeOf[A]
        val tree = q"new MyTypeclass[$typA] {}"
        if (!mtcCache.contains(typA)) 
          mtcCache += (typA -> (tree, c.typecheck(tree)))
    
        mtcCache(typA)._1.asInstanceOf[Tree]
      }
    }
    

    or if you need typechecking only to trigger the recursion then you can typecheck a tree, put the untyped tree to the cache and return the untyped one

    val mtcCache = mutable.Map[whitebox.Context#Type, whitebox.Context#Tree]()
    
    trait MyTypeclass[A]
    
    object MyTypeclass {
      implicit def materialize[A]: MyTypeclass[A] = macro materializeImpl[A]
    
      def materializeImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
    
        val typA = weakTypeOf[A]
        val tree = q"new MyTypeclass[$typA] {}"
        c.typecheck(tree)
        if (!mtcCache.contains(typA)) mtcCache += (typA -> tree)
    
        mtcCache(typA).asInstanceOf[Tree]
      }
    }
    

    Pull request: https://github.com/readren/json-facile/pull/1