Link Search Menu Expand Document

编译器插件的变化

从 Dotty 0.9 开始,Dotty(Scala 3)开始支持编译器插件。与 Scala 2 的 scala 相比, 这有两个显著的变化:

  • 不支持 analyzer 插件
  • 添加了对 research 插件的支持

scalac 中的Analyzer 插件在类型检查期间运行,可能会影响正常的类型检查。 这是一个非常强大的特性,但是对于生产用途,可预测的、一致的类型检查更为重要。

为了实验和研究,Scala 3 引入了 research 插件。Research 插件比 scalac 的 analyzer 插件更强大, 因为它允许插件作者定制整个编译器 pipeline。可以很容易用定制的 typer 替代标准的 typer, 或为 DSL 创建一个解析器。不过,research 插件只在 Scala 3 的 nightly 或 snaphot 版本中启用。

向编译器 pipeline 添加新的阶段的常见插件在 Scala 3 中被称为标准插件(standard plugin)。 特性上它们与 scalac 插件类似,但是 API 有一些小变化。

使用编译器插件

标准插件和 research 插件都可以通过为 scalac 添加 -Xplugin: 选项启用:

scalac -Xplugin:pluginA.jar -Xplugin:pluginB.jar Test.scala

编译器将检查所提供的 jar,在 jar 的根目录中查找名为 plugin.properties 的属性文件。 属性文件指定了插件类的完全限定类名。属性文件的格式如下:

pluginClass=dividezero.DivideZero

这与需要 scalac-plugin.xml 文件的 scalac 插件不同。

从 1.1.5 开始,sbt 支持 Scala 3 编译器插件。更多信息请参见 sbt 文档

编写标准编译器插件

下面是一个简单的编译器插件的源码,该插件将整数除以零作为错误报告。

package dividezero

import dotty.tools.dotc.ast.Trees.*
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Decorators.*
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin}
import dotty.tools.dotc.transform.{Pickler, Staging}

class DivideZero extends StandardPlugin {
   val name: String = "divideZero"
   override val description: String = "divide zero check"

   def init(options: List[String]): List[PluginPhase] =
      (new DivideZeroPhase) :: Nil
}

class DivideZeroPhase extends PluginPhase {
   import tpd.*

   val phaseName = "divideZero"

   override val runsAfter = Set(Pickler.name)
   override val runsBefore = Set(Staging.name)

   override def transformApply(tree: Apply)(implicit ctx: Context): Tree = {
      tree match {
         case Apply(Select(rcvr, nme.DIV), List(Literal(Constant(0))))
         if rcvr.tpe <:< defn.IntType =>
            report.error("dividing by zero", tree.pos)
         case _ =>
            ()
      }
      tree
   }
}

插件的主类(DivideZero)必须继承 trait StandardPlugin,并实现 init 方法, 该方法接受插件的选项作为参数,返回要插入编译器 pipeline 的 PluginPhase 列表。

我们的插件向 pipeline 插入了一个编译器阶段。编译器阶段必须继承 trait PluginPhase。 为了指定何时执行阶段,我们还需要指定约束 runsBeforerunsAfter,它们是阶段名称的列表。

我们现在可以通过覆盖像 transformXXX 这样的方法转换树。

编写 Research 编译器插件

这里是一个 research 插件的模板。

import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Phases.Phase
import dotty.tools.dotc.plugins.ResearchPlugin

class DummyResearchPlugin extends ResearchPlugin {
   val name: String = "dummy"
   override val description: String = "dummy research plugin"

   def init(options: List[String], phases: List[List[Phase]])(implicit ctx: Context): List[List[Phase]] =
      phases
}

A research plugin must extend the trait ResearchPlugin and implement the method init that takes the plugin’s options as argument as well as the compiler pipeline in the form of a list of compiler phases. The method can replace, remove or add any phases to the pipeline and return the updated pipeline.