0

Is there a way to generate a method within a particular class/trait/object based on TypesafeConfig object at compile time?

For instance, I have this:

object Main {
  val config: Config = ConfigFactory.parseString(
    """
      |object {
      |  name = "go"
      |}
    """.stripMargin)

  generate(config)
}

And the expected result is:

object Main {
  val config: Config = ConfigFactory.parseString(
    """
      |object {
      |  name = "go"
      |}
    """.stripMargin)

  def method: Unit = {
    print("go") /* the string comes from the config above */
  }
}

The idea is to be able to instantiate the Config object within the scope of macro implementation and use its properties to generate the code, e.g. (many thanks to Dmytro Minin for the example):

object GenerateMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    confObj = ... /* somehow get the real object based on macro input */        

    annottees match {
      case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
        q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
           ..$body

           def method: Unit = {
             print(s"${confObj.getString("object.name")}") /* use confObj's property to "embed" the value into the method generated */
           }
        }"""
    }
  }
}
Pavel
  • 11
  • 2

1 Answers1

0

You can create macro annotation:

macros/scr/main/scala/generate.scala

import com.typesafe.config.Config
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class generate extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro GenerateMacro.impl
}

object GenerateMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._


    val confObj: Config = c.prefix.tree match {
      case q"new generate($config)" => c.eval(c.Expr[Config](/*c.untypecheck(*/config/*)*/))
    }

    println(confObj) //Config(SimpleConfigObject({"object":{"name":"go"}}))

    annottees match {
      case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
        q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
         ..$body

         def method: Unit = {
           print(${confObj.getString("object.name")})
         }
      }"""
    }
  }
}

and use it:

core/scr/main/scala/Main.scala

@generate(com.typesafe.config.ConfigFactory.parseString(
  """
    |object {
    |  name = "go"
    |}
  """.stripMargin))
object Main

Then you can do

Main.method //go

build.sbt

scalaVersion := "2.12.6"

lazy val commonSettings = Seq(
  addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
)

lazy val macros: Project = (project in file("macros")).settings(
  commonSettings,
  libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
)

lazy val core: Project = (project in file("core")).aggregate(macros).dependsOn(macros).settings(
  commonSettings,
  libraryDependencies += "com.typesafe" % "config" % "1.3.2"
)
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks, man, but the solution you provided is not exactly what I want to achieve here. I want to "pass" the config object to the macro implementation level to be able to generate the method by using the config from outside, not the method which has been generated to use config as a class member. What I mean is as follows: when GenerateMacro.impl is being called I want to: - instantiate TypesafeConfig object somehow - use its attributes to generate the method (e.g.: not just print(config.getString("object.name")) but like that: val name = confObj.getString("object.name") print(s"$val")) – Pavel Jul 11 '18 at 10:16
  • Either add parameter to annotation: `class generate(config: Config) extends StaticAnnotation ...` or write def macro instead of annotation if this is enough fo you: `def generate(config: Config): Unit = macro generateImpl; def generateImpl(c: blackbox.Context)(config: c.Tree): c.Tree = ...` – Dmytro Mitin Jul 11 '18 at 11:42
  • Actually, that's exactly the question: is there a way to transform `config: c.Tree` to `config: Config`? For example, is a string parameter is passed explicitly (`"value"`) there is a way to get the value by using `Eval[String]`, any suggestions regarding Config? – Pavel Jul 11 '18 at 12:49
  • `val cnfg: Config = c.eval[Config](c.Expr(config))` Do you mean this? – Dmytro Mitin Jul 11 '18 at 13:05
  • Sort of, but in this case the code generates an exception: `scala.tools.reflect.ToolBoxError: reflective toolbox has failed: cannot operate on trees that are already typed` And the code is pretty small: `def generateConfigImpl(c: blackbox.Context)(entity: c.Tree): c.Tree = { import c.universe._ val cnfg: Config = c.eval[Config](c.Expr(entity)) q"""println("go")""" }` – Pavel Jul 12 '18 at 05:27
  • I updated my answer. Try it now. You should `untypecheck` a tree before `eval` it. Refer [scaladoc](https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/macros/Evals.scala) for `eval`. – Dmytro Mitin Jul 14 '18 at 18:45
  • There is stronger version of `untypecheck`, i.e. `resetallattrs`: https://github.com/scalamacros/resetallattrs You should add `libraryDependencies += "org.scalamacros" %% "resetallattrs" % "1.0.0"` to `build.sbt` and use it: `import org.scalamacros.resetallattrs._` `val cnfg: Config = c.eval(c.Expr[Config](c.resetAllAttrs(config)))` – Dmytro Mitin Jul 14 '18 at 19:25