Edit this page on GitHub

Changes in Compiler Plugins

Compiler plugins are supported in Scala 3 since Dotty 0.9. There are two notable changes compared to Scala 2:

  • No support for analyzer plugins
  • Added support for research plugins

Analyzer plugins run in Scala 2 during type checking and may influence normal type checking. This is a very powerful feature but for production usages, a predictable and consistent type checker is more important.

For experimentation and research, Scala 3 introduces research plugin. Research plugins are more powerful than Scala 2 analyzer plugins as they let plugin authors customize the whole compiler pipeline. One can easily replace the standard typer by a custom one or create a parser for a domain-specific language. However, research plugins are only enabled for nightly or snaphot releases of Scala 3.

Common plugins that add new phases to the compiler pipeline are called standard plugins in Scala 3. In terms of features, they are similar to scalac plugins, despite minor changes in the API.

Using Compiler Plugins

In Scala 3, both standard and research plugins can be used with scalac by adding the -Xplugin: option:

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

The compiler will examine the jar provided, and look for a property file named plugin.properties in the root directory of the jar. The property file specifies the fully qualified plugin class name. The format of a property file is as follows:

pluginClass=dividezero.DivideZero

This is different from Scala 2 plugins that require a scalac-plugin.xml file.

Starting from 1.1.5, sbt also supports Scala 3 compiler plugins. Please refer to the sbt documentation for more information.

Writing a Standard Compiler Plugin

Here is the source code for a simple compiler plugin that reports integer divisions by zero as errors.

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

The plugin main class (DivideZero) must extend the trait StandardPlugin and implement the method init that takes the plugin's options as argument and returns a list of PluginPhases to be inserted into the compilation pipeline.

Our plugin adds one compiler phase to the pipeline. A compiler phase must extend the PluginPhase trait. In order to specify when the phase is executed, we also need to specify a runsBefore and runsAfter constraints that are list of phase names.

We can now transform trees by overriding methods like transformXXX.

Writing a Research Compiler Plugin

Here is a template for research plugins.

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

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.