Scala 宏类型检查如何将标识符解析为类型?

How do Scala macro typechecks resolve identifiers to types?

我正在尝试创建一个只能应用于特定类型的注释宏。当我 运行 我的测试时,当注释仅应用于顶级对象时,我看到一个类型未找到错误。

我的宏代码:

trait Labelled[T] {
  def label: T
}

@compileTimeOnly("DoSomethingToLabelled requires the macro paradise plugin")
class DoSomethingToLabelled extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro DoSomethingToLabelled.impl
}

object DoSomethingToLabelled {
  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._

    annottees.map(_.tree).head match {
      case expr @ ModuleDef(mods: Modifiers, name: TermName, impl: Template) =>
        println(showRaw(impl.parents))
        val parentTypes = impl.parents.map(c.typecheck(_, c.TYPEmode))

        if (parentTypes.exists(_.tpe <:< typeOf[Labelled[_]])) {
          c.Expr[Any](expr)
        } else {
          c.abort(c.enclosingPosition, s"DoSomethingToLabelled can only be applied to a Labelled. Received types: $parentTypes")
        }
    }
  }
}

我的测试代码:

class DoSomethingToLabelledSpec extends Specification {

  private def classPathUrls(cl: ClassLoader): List[String] = cl match {
    case null => Nil
    case u: java.net.URLClassLoader => u.getURLs.toList.map(systemPath) ++ classPathUrls(cl.getParent)
    case _ => classPathUrls(cl.getParent)
  }

  private def systemPath(url: URL): String = {
    Paths.get(url.toURI).toString
  }

  private def paradiseJarLocation: String = {
    val classPath = classPathUrls(getClass.getClassLoader)
    classPath.find(_.contains("paradise")).getOrElse {
      throw new RuntimeException(s"Could not find macro paradise on the classpath: ${classPath.mkString(";")}")
    }
  }

  lazy val toolbox = runtimeMirror(getClass.getClassLoader)
    .mkToolBox(options = s"-Xplugin:$paradiseJarLocation -Xplugin-require:macroparadise")

  "The DoSomethingToLabelled annotation macro" should {

    "be applicable for nested object definitions extending Labelled" in {
      toolbox.compile {
        toolbox.parse {
          """
            |import macrotests.Labelled
            |import macrotests.DoSomethingToLabelled
            |
            |object Stuff {
            |  @DoSomethingToLabelled
            |  object LabelledWithHmm extends Labelled[String] {
            |    override val label = "hmm"
            |  }
            |}
            |""".stripMargin
        }
      } should not (throwAn[Exception])
    }

    "be applicable for top level object definitions extending Labelled" in {
      toolbox.compile {
        toolbox.parse {
          """
            |import macrotests.Labelled
            |import macrotests.DoSomethingToLabelled
            |
            |@DoSomethingToLabelled
            |object LabelledWithHmm extends Labelled[String] {
            |  override val label = "hmm"
            |}
            |""".stripMargin
        }
      } should not (throwAn[Exception])
    }
  }
}

而我的测试日志是:

sbt:macro-type-extraction> test
[info] Compiling 1 Scala source to C:\Users\WilliamCarter\workspace\macro-type-extraction\target\scala-2.11\classes ...
[info] Done compiling.
List(AppliedTypeTree(Ident(TypeName("Labelled")), List(Ident(TypeName("String")))))
List(AppliedTypeTree(Ident(TypeName("Labelled")), List(Ident(TypeName("String")))))
[info] DoSomethingToLabelledSpec
[info] The DoSomethingToLabelled annotation macro should
[info]   + be applicable for nested object definitions extending Labelled
[error] scala.tools.reflect.ToolBoxError: reflective compilation has failed:
[error]
[error] exception during macro expansion:
[error] scala.reflect.macros.TypecheckException: not found: type Labelled
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$$anonfun$apply.apply(Typers.scala:34)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$$anonfun$apply.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun.apply(Typers.scala:24)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun.apply(Typers.scala:24)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$withContext.apply(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$withContext.apply(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun.apply(Typers.scala:23)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun.apply(Typers.scala:23)
[error]         at scala.reflect.macros.contexts.Typers$class.withContext(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$class.withWrapping(Typers.scala:26)
[error]         at scala.reflect.macros.contexts.Typers$class.typecheck(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
[error]         at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
[error]         at macrotests.DoSomethingToLabelled$$anonfun.apply(DoSomethingToLabelled.scala:19)
[error]         at macrotests.DoSomethingToLabelled$$anonfun.apply(DoSomethingToLabelled.scala:19)
[error]         at scala.collection.immutable.List.map(List.scala:284)
[error]         at macrotests.DoSomethingToLabelled$.impl(DoSomethingToLabelled.scala:19)

我的调试打印告诉我提取的父类型在每个测试中都是相同的,但由于某种原因,顶级对象无法解析 TypeName("Labelled") 实际上是 macrotests.Labelled。有没有人能在这里帮助阐明一些问题?该宏似乎在测试上下文之外工作,但我真的很想了解发生了什么,以便我可以编写一些适当的测试。

尝试

toolbox.compile {
  toolbox.parse {
    """
      |import macrotests.DoSomethingToLabelled
      |
      |@DoSomethingToLabelled
      |object LabelledWithHmm extends macrotests.Labelled[String] {
      |  override val label = "hmm"
      |}
      |""".stripMargin
  }
}

甚至

toolbox.compile {
  toolbox.parse {
    """
      |import macrotests.DoSomethingToLabelled
      |
      |@DoSomethingToLabelled
      |object LabelledWithHmm extends _root_.macrotests.Labelled[String] {
      |  override val label = "hmm"
      |}
      |""".stripMargin
  }
}

对了,为什么需要工具箱?为什么不只写

@DoSomethingToLabelled
object LabelledWithHmm extends Labelled[String] {
  override val label = "hmm"
}

在测试中?那么代码编译的事实将在编译时而不是在运行时使用工具箱进行检查。

https://github.com/scala/bug/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+%28toolbox+%26%26+%28import+%7C%7C+package%29%29

https://github.com/scala/bug/issues/6393

@xeno-by said: It looks like we're doomed w.r.t this one.

The problem is that Scala reflection and reflective compiler (which is underlying toolboxes) use a different model of classfile loading than vanilla scalac does. Vanilla compiler has its classpath as a list of directories/jars on the filesystem, so it can exhaustively enumerate the packages on the classpath. Reflective compiler works with arbitrary classloaders, and classloaders don't have a concept of enumerating packages.

As a result, when a reflective compiler sees "math" having "import scala.; import java.lang." imports in the lexical context, it doesn't know whether that "math" stands for root.math, scala.math or java.lang.math. So it has to speculate and provisionally creates a package for root.math, which ends up being a wrong choice.

We could probably support a notion of "overloaded" packages, so that the compiler doesn't have to speculate and can store all the possible options, but that'd require redesign of reflection and probably of the typer as well.