在 GitHub 上编辑此页面

编译器插件的变化

从 Dotty 0.9 开始,Scala 3 支持编译器插件。与 Scala 2 相比,有两个显著的更改

  • 不支持分析器插件
  • 添加了对研究插件的支持

分析器插件 在 Scala 2 中的类型检查期间运行,并且可能会影响正常的类型检查。这是一个非常强大的功能,但对于生产用途,可预测且一致的类型检查器更为重要。

为了进行实验和研究,Scala 3 引入了研究插件。研究插件比 Scala 2 分析器插件更强大,因为它们允许插件作者自定义整个编译器管道。可以轻松地用自定义类型检查器替换标准类型检查器,或为特定领域语言创建解析器。但是,研究插件仅在使用-experimental 编译器标志或在 Scala 3 的夜间/快照版本中启用。

在 Scala 3 中,向编译器管道添加新阶段的通用插件称为标准插件。在功能方面,它们类似于scalac 插件,尽管 API 有细微的更改。

使用编译器插件

在 Scala 3 中,标准插件和研究插件都可以通过添加 -Xplugin: 选项与 scalac 一起使用。

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

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

pluginClass=dividezero.DivideZero

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

从 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
end DivideZeroPhase

插件主类 (DivideZero) 必须扩展 StandardPlugin 特性并实现 init 方法,该方法以插件选项作为参数,并返回要插入编译管道的 PluginPhase 列表。

我们的插件向管道添加了一个编译阶段。编译阶段必须扩展 PluginPhase 特性。为了指定阶段执行的时间,我们还需要指定 runsBeforerunsAfter 约束,它们是阶段名称列表。

我们现在可以通过覆盖 transformXXX 等方法来转换树。

编写研究编译器插件

以下是一个研究插件的模板。

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
end DummyResearchPlugin

研究插件必须扩展 ResearchPlugin 特性并实现 init 方法,该方法以插件选项作为参数,以及以编译阶段列表形式的编译管道。该方法可以替换、删除或添加管道中的任何阶段,并返回更新后的管道。