Search code examples
scalascala-macrosstructural-typingscalametasemanticdb

Is it possible to using macro to modify the generated code of structural-typing instance invocation?


For example as the following code:

object Test extends App
{
    trait Class
    {
        val f1: Int
    }

    val c = new Class {
        val f1: Int = 1
        val f2: String = "Class"
    }

    println(c.f1)
    println(c.f2)
}

I look into the bytecode with a decompiler, and notice that the compile generate a java interface 'Test.Class' as pseudo code:

trait Class
{
    val f1: Int
}

and a class 'Test$$anon$1' implemeting 'Test.Class', pseudo code as:

class Test$$anon$1 extends Class
{
    val f1: Int = 1
    val f2: String = "Class"
}

and then the compiler initiaize the variable 'c' as:

c = new Test$$anon$1()

then calls the member 'f1' as normal invocation:

println(c.f1)

but it calls 'f2' using reflection:

println(reflMethod(c, f2))

Here, since the definition of the anonymous class 'Test$$anon$1' is visible in the same scope, is it possible to use macro to change the generated code to invoke 'f2' as normal field avoiding reflection?

I just want to change the invocation code in the same scope, not want to change the reflection code across scopes e.g. structual-typing instance as argument in function call. So I think it is possible in theory. But I am not familiar with scala macro, suggestions and code examples are appreciated. Thanks!


Solution

  • Macros (more precisely, macro annotations because def macros are irrelevant to this task) are not enough. You want to rewrite not class (trait, object) or its parameter or member but local expressions. You can do this either with compiler plugin (see also) at compile time or with Scalameta code generation before compile time.

    If you choose Scalameta then actually you want to rewrite your expressions semantically rather than syntactically because you want to go from local expression new Class... to the definition trait Class... and check whether there are proper members there. So you need Scalameta + SemanticDB. More convenient is to use Scalameta + SemanticDB with Scalafix (see also section for users).

    You can create your own rewriting rule. Then you can use it either for rewriting your code in-place or for code generation (see below).

    rules/src/main/scala/MyRule.scala

    import scalafix.v1._
    import scala.meta._
    
    class MyRule extends SemanticRule("MyRule") {
      override def isRewrite: Boolean = true
    
      override def description: String = "My Rule"
    
      override def fix(implicit doc: SemanticDocument): Patch = {
        doc.tree.collect {
          case tree @ q"new { ..$stats } with ..$inits { $self => ..$stats1 }" =>
            val symbols = stats1.collect {
              case q"..$mods val ..${List(p"$name")}: $tpeopt = $expr" =>
                name.syntax
            }
    
            val symbols1 = inits.headOption.flatMap(_.symbol.info).flatMap(_.signature match {
              case ClassSignature(type_parameters, parents, self, declarations) =>
                Some(declarations.map(_.symbol.displayName))
              case _ => None
            })
    
            symbols1 match {
              case None => Patch.empty
              case Some(symbols1) if symbols.forall(symbols1.contains) => Patch.empty
              case _ =>
                val anon = Type.fresh("anon$meta$")
                val tree1 =
                  q"""
                    class $anon extends ${template"{ ..$stats } with ..$inits { $self => ..$stats1 }"}
                    new ${init"$anon()"}
                  """
                Patch.replaceTree(tree, tree1.syntax)
            }
        }.asPatch
      }
    }
    

    in/src/main/scala/Test.scala

    object Test extends App
    {
      trait Class
      {
        val f1: Int
      }
    
      val c = new Class {
        val f1: Int = 1
        val f2: String = "Class"
      }
    
      println(c.f1)
      println(c.f2)
    }
    

    out/target/scala-2.13/src_managed/main/scala/Test.scala (after sbt out/compile)

    object Test extends App
    {
      trait Class
      {
        val f1: Int
      }
    
      val c = {
      class anon$meta$2 extends Class {
        val f1: Int = 1
        val f2: String = "Class"
      }
      new anon$meta$2()
    }
    
      println(c.f1)
      println(c.f2)
    }
    

    build.sbt

    name := "scalafix-codegen-demo"
    
    inThisBuild(
      List(
        scalaVersion := "2.13.2",
        addCompilerPlugin(scalafixSemanticdb),
        scalacOptions ++= List(
          "-Yrangepos"
        )
      )
    )
    
    lazy val rules = project
      .settings(
        libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.9.16"
      )
    
    lazy val in = project
    
    lazy val out = project
      .settings(
        sourceGenerators.in(Compile) += Def.taskDyn {
          val root = baseDirectory.in(ThisBuild).value.toURI.toString
          val from = sourceDirectory.in(in, Compile).value
          val to = sourceManaged.in(Compile).value
          val outFrom = from.toURI.toString.stripSuffix("/").stripPrefix(root)
          val outTo = to.toURI.toString.stripSuffix("/").stripPrefix(root)
          Def.task {
            scalafix
              .in(in, Compile)
    //          .toTask(s" ProcedureSyntax --out-from=$outFrom --out-to=$outTo")
              .toTask(s" --rules=file:rules/src/main/scala/MyRule.scala --out-from=$outFrom --out-to=$outTo")
              .value
            (to ** "*.scala").get
          }
        }.taskValue
      )
    

    project/plugins.sbt

    addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")
    

    Other examples:

    https://github.com/olafurpg/scalafix-codegen

    https://github.com/DmytroMitin/scalafix-codegen

    https://github.com/DmytroMitin/scalameta-demo

    Scala conditional compilation

    Macro annotation to override toString of Scala function

    How to merge multiple imports in scala?