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

307 views Asked by At

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!

1

There are 1 answers

1
Dmytro Mitin On BEST ANSWER

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?