Search code examples
scalascala-reflectruntime-compilationscala-quasiquotesquasiquotes

How to runtime compile with reflection for a class being used within another class


My code:

import scala.reflect.runtime
import scala.reflect.runtime.universe
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox

object Stuff {
  val rm: universe.Mirror = runtime.currentMirror
  val tb: ToolBox[universe.type] = rm.mkToolBox()
  val Example1 = tq"namespace.to.Example1"
  val Example2 = tq"namespace.to.Example2"

  val classDef = tb
    .define(
     q"""
     case class MyClass extends $Example1 { // 
       def doWork(): $Example2 = {
         Example2("hello") // <-- I believe this is the offending line
       }
     }
     """.asInstanceOf[ClassDef]
  ).asClass
}

where Example2:

case class Example2(member: String)

I run into this error:

Cause: scala.tools.reflect.ToolBoxError: reflective compilation has failed:
not found: value Example2

I've tried this too:

       def doWork(): $Example2 = {
         $Example2("hello")
       }

but that runs into:

class namespace.to.Example2 is not a value

Using $Example2 as the type of the method works, but how do I make the reflection understand creating an instance of Example2?

EDIT:

How would I also reference Example2 if it were another class that I had to compile at runtime? If I want this at runtime without having a namespace.to.Example2 (class doesn't exist)

case class Example2(member: String) {
  def aMe: Example2 = {
    // do any work
  }
}

object Stuff {
  val rm: universe.Mirror = runtime.currentMirror
  val tb: ToolBox[universe.type] = rm.mkToolBox()
  val classDef = tb
    .define(
    q"""  
    case class MyClass {   
      def doWork(): $Example2 = {
        val ex = new $Example2("hello") // <-- this would not work?
        ex.aMethod()
      }
    }
}

Because there is no val Example2 = tq"namespace.to.Example2", the new Example2("hello") probably doesn't work.


Solution

  • Use splicing (${...}) and either the constructor of case class

    val Example1T = tq"namespace.to.Example1"
    val Example2T = tq"namespace.to.Example2"
    
    val classSym = tb.define(
      q"""
        case class MyClass() extends $Example1T {
          def doWork(): $Example2T = {
            new $Example2T("hello")
          }
        }
      """.asInstanceOf[ClassDef]
    ).asClass
    

    or apply method of companion object

    val Example1T    = tq"namespace.to.Example1"
    val Example2T    = tq"namespace.to.Example2"
    val Example2ObjT = q"namespace.to.Example2"
    
    val classSym = tb.define(
      q"""
        case class MyClass() extends $Example1T {
          def doWork(): $Example2T = {
            $Example2ObjT("hello")
          }
        }
      """.asInstanceOf[ClassDef]
    ).asClass
    

    The string interpolator tq"..." creates a type tree, q"..." creates a term tree

    https://docs.scala-lang.org/overviews/quasiquotes/intro.html

    If Example1 and Example2 are ordinary classes (not runtime-generated) you can use them statically

    // val Example1T = typeOf[Example1]
    val Example1T = symbolOf[Example1]
    // val Example2T = typeOf[Example2]
    val Example2T = symbolOf[Example2]
    // val Example2ObjT = symbolOf[Example2.type].asClass.module
    val Example2ObjT = Example2T.companion
    

    Normally you can use imports inside quasiquotes with tb.eval, tb.compile, tb.typecheck

    q"""
      import namespace.to._
      new Example2("hello")
      Example2("hello")
    """
    

    but not with tb.define because it accepts ClassDef/ModuleDef (<: ImplDef <: Tree) rather than arbitrary Tree and adding imports makes a tree a Block (<: Tree).


    How would I also reference Example2 if it were another class that I had to compile at runtime?

    Make use of the class symbol returned by tb.define on the previous step (you can splice trees, symbols, types into trees)

    val example2ClassSym = tb.define(
      q"""
        case class Example2(member: String) {
          def aMethod(): Example2 = {
            Example2("aaa")
          }
        }
      """.asInstanceOf[ClassDef]
    ).asClass
    
    val myClassSym = tb.define(
      q"""
        case class MyClass() {
          def doWork(): $example2ClassSym = {
            val ex = new $example2ClassSym("hello")
            ex.aMethod()
          }
        }
      """.asInstanceOf[ClassDef]
    ).asClass
    

    Or if this is enough, just

    tb.eval(
      q"""
        val ex = new $example2ClassSym("hello")
        ex.aMethod()
      """
    )
    
    tb.eval(
      q"""
        import ${example2ClassSym.owner.asClass.module}._
        val ex = new Example2("hello")
        ex.aMethod()
      """
    )