컴파일러 플러그인의 변경 사항

컴파일러 플러그인의 변경 사항

컴파일러 플러그인은 Dotty 0.9부터 Scala 3에서 지원돼요. Scala 2와 비교하면 두 가지 눈에 띄는 변화가 있죠.

출처: Scala 3 Reference

본문

분석 플러그인(Analyzer Plugin)은 더 이상 지원되지 않아요

Scala 2에서 분석 플러그인은 타입 검사 중에 실행되면서 타입 검사 흐름 자체에 영향을 줄 수 있었어요. 아주 강력한 기능이긴 하지만, 프로덕션 환경에서는 예측 가능하고 일관된 타입 검사기가 더 중요하죠.

연구 플러그인(Research Plugin)이 새로 추가됐어요

실험과 연구를 위해 Scala 3는 연구 플러그인을 도입했어요. 연구 플러그인은 Scala 2의 분석 플러그인보다 훨씬 강력한데, 플러그인 작성자가 컴파일러 파이프라인 전체를 커스터마이즈할 수 있기 때문이에요. 기본 타입 검사기를 직접 만든 것으로 교체하거나, 도메인 특화 언어(domain-specific language)를 위한 파서를 만들 수도 있어요. 다만 연구 플러그인은 Scala 3의 nightly나 snapshot 릴리스에서만 활성화돼요.

컴파일러 파이프라인에 새 단계(phase)를 추가하는 일반적인 플러그인은 Scala 3에서 표준 플러그인(standard plugin) 이라고 불러요. 기능 면에서는 scalac 플러그인과 비슷하고, API에 약간의 변화만 있을 뿐이에요.

컴파일러 플러그인 사용하기

Scala 3에서는 표준 플러그인과 연구 플러그인 모두 scalac에서 -Xplugin: 옵션을 붙여 사용할 수 있어요.

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

컴파일러는 전달받은 jar를 살펴보고, jar 루트 디렉터리에서 plugin.properties라는 프로퍼티 파일을 찾아요. 이 프로퍼티 파일이 플러그인 클래스의 완전한 이름(fully qualified name)을 지정해 주죠. 프로퍼티 파일의 형식은 다음과 같아요.

pluginClass=dividezero.DivideZero

이건 Scala 2 플러그인이 scalac-plugin.xml 파일을 요구했던 것과는 다른 점이에요.

1.1.5부터 sbt도 Scala 3 컴파일러 플러그인을 지원해요. 자세한 내용은 sbt 문서를 참고해 주세요.

표준 컴파일러 플러그인 작성하기

다음은 정수 나눗셈에서 0으로 나누는 경우를 오류로 보고하는 간단한 컴파일러 플러그인의 소스 코드예요.

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"

  override def initialize(options: List[String])(using Context): 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

플러그인의 메인 클래스인 DivideZeroStandardPlugin 트레이트를 상속해야 하고, initialize 메서드를 구현해야 해요. 이 메서드는 플러그인의 옵션을 인자로 받아서, 컴파일 파이프라인에 삽입할 PluginPhase 목록을 반환해요.

우리 플러그인은 파이프라인에 컴파일러 단계(phase)를 하나 추가하죠. 컴파일러 단계는 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 메서드를 구현해야 해요. 이 메서드는 플러그인의 옵션을 인자로 받고, 컴파일러 단계들의 목록 형태로 된 컴파일러 파이프라인도 인자로 받아요. 이 메서드에서 파이프라인의 어떤 단계든 교체·제거·추가할 수 있고, 갱신된 파이프라인을 반환하면 되는 거예요.

더 알아보기 (Learn more)