この記事は Scala Advent Calendar 2020 - Qiita 18日目の記事です。
完全に自分用のメモなのですが、コンパイラ周辺ツールの Scala3 対応に備えて、dotty の interactive compiler API の使い方を一部学んでみたのでその学習メモを残しておく。
今回試したコードは以下のリポジトリにまとまっていますが、このブログでは試行錯誤した過程を書いていこうと思います。今回は使い方だけ見たけど次はどう実装されてるかまで読んでいきたい。
github.com
もうちょっと雑なやつ dotty tools で遊んでみる - tanishiking-pub
また使い方を勉強するにあたっては dotty language server を参考にしました。 dotty/DottyLanguageServer.scala at eddd4da41ac14057edf4db6f9a24de6f768dbbb3 · lampepfl/dotty
(dotty language server はあくまで参考実装という感じで、将来的には変わらず intellij-scala と metals が scala3 対応を頑張っていく感じになるんではなかろうか? dotty-language-server は最低限の機能は備えているが、実プロジェクトで利用するにはいろいろと機能が不足している割に暫く手が入ってないので (要出典))
dotty tools の主なエンドポイントは InteractiveDriver
というクラス
project を作る
sbt console
で遊んでみよう。まずは砂場とするプロジェクトを作る
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.6")
val dottyVersion = "3.0.0-M2"
lazy val root = project
.in(file("."))
.settings(
name := "dotty-interactive-playground",
version := "0.1.0",
scalaVersion := dottyVersion,
libraryDependencies ++= List(
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value,
"io.get-coursier" % "interface" % "1.0.1",
"com.lihaoyi" %% "pprint" % "0.6.0",
)
)
このproject内で sbt console
を実行してみる。
InteractiveDriver の instantiate
scala> import dotty.tools.dotc.interactive.InteractiveDriver
scala> val driver = new InteractiveDriver(List.empty)
dotty.tools.dotc.MissingCoreLibraryException: Could not find package scalaShadowing from compiler core libraries.
Make sure the compiler core libraries are on the classpath.
なるほど、scala3-library
の classpath を与えないと死ぬようになってるのか
The official standard library for Scala 3.0 is the Scala 2.13 library. Not only the source code is unchanged but it is even not compiled and published under 3.0. It could be, but it would be useless because, as we have seen, a Scala 3.0 module can depend on a Scala 2.13 artifact.
https://scalacenter.github.io/scala-3-migration-guide/docs/compatibility.html#the-scala-standard-library
とあるように、scala3の標準ライブラリは2.13のライブラリを利用しているため、scala3-library
の classpath に加えて、scala2.13 の標準ライブラリのクラスパスも与えて
scala-library
の classpath ってどこにあるんだっけ?
- sbt の依存に dotty-library
が含まれている場合 sbt show runtime:fullClasspath
で依存しているライブラリのclasspathが分かるのでコピペできる
scala> val classpaths = Seq(
"/Users/tanishiking/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala3-library_3.0.0-M2/3.0.0-M2/scala3-library_3.0.0-M2-3.0.0-M2.jar",
"/Users/tanishiking/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.4/scala-library-2.13.4.jar"
)
scala> val driver = new InteractiveDriver(List("-classpath", classpaths.mkString(java.io.File.pathSeparator)))
val driver: dotty.tools.dotc.interactive.InteractiveDriver = dotty.tools.dotc.interactive.InteractiveDriver@4123624c
もしくは別にこんなことしなくても coursier の fetch API で必要なライブラリを取得してきて、そのクラスパスをInteractiveDriverに食わせてやる
import coursierapi.{Fetch, Dependency}
import java.nio.file.Path
val fetch = Fetch.create()
import scala.jdk.CollectionConverters._
fetch.addDependencies(
Dependency.of("org.scala-lang", "scala3-library_3.0.0-M2", "3.0.0-M2")
)
val extraLibraries: Seq[Path] = fetch
.fetch()
.asScala
.map(_.toPath())
.toSeq
val driver = new InteractiveDriver(
List(
"-color:never",
"-classpath",
extraLibraries.mkString(java.io.File.pathSeparator)
)
)
InteractiveDriver を使っていろんなコードを interactive に解析してみよう
val uri = new URI("file:///virtual")
driver.run(uri, "object X { }")
pprint.log(driver.openedFiles)
pprint.log(driver.openedTrees(uri))
エラーがあるコードを与えてみる
val sourceParital = "object X { def x = 1.toSt } "
val uriPartial = new URI("file:///partial")
val diag = driver.run(uriPartial, sourceParital)
pprint.log(diag)
エラーが帰ってきた。しかし openedTree には error symbol っぽいものを使ってエラー回復したと思われる部分的な構文木が登録されている。これのおかげで部分的なコードでも補完とかnavigationが実行できるわけだね
pprint.log(driver.openedTrees(uriPartial))
completion の実行
Completions.completion
を使って、指定したポジションで completion API を実行してみる。さっきのコードの 1.toS
のところで補完を実行してみよう。
import dotty.tools.dotc.interactive.{InteractiveDriver, Interactive, Completion}
import dotty.tools.dotc.util.{Spans, SourcePosition}
import dotty.tools.dotc.core.Contexts._
val pos = new SourcePosition(
driver.openedFiles(uriPartial),
Spans.Span(sourceParital.indexOf(".toSt") + ".toS".length)
)
val completions = Completion.completions(pos)(using driver.currentCtx.fresh.setCompilationUnit(driver.compilationUnits.get(uriPartial).get))
pprint.log(completions)
toString
と toShort
が補完された
find definition
次は定義ジャンプなんかを実装するために利用する Definition.findDefinition
を使って、あるシンボルの定義元を探す機能を利用してみる。
val sourceDefinition = "object Definition { def x = 1; val y = x + 1 }"
val uriDefinition = new URI("file:///def")
driver.run(uriDefinition, sourceDefinition)
given ctx as Context = driver.currentCtx
val pos = new SourcePosition(driver.openedFiles(uriDefinition), Spans.Span(sourceDefinition.indexOf("x + 1")))
val path = Interactive.pathTo(driver.openedTrees(uriDefinition), pos)
// Feeding path to the pos, and return definition's tree
val definitions = Interactive.findDefinitions(path, pos, driver)
pprint.log(definitions)
// List(
// SourceTree(
// tree = DefDef(
// name = x,
// tparams = List(),
// vparamss = List(),
// tpt = TypeTree[dotty.tools.dotc.core.Types$PreviousErrorType@6b6b68d0],
// preRhs = Select(qualifier = Literal(const = ( = 1)), name = toSt)
// ),
// source = /partial
// ),
// SourceTree(
// tree = DefDef(
// name = x,
// tparams = List(),
// vparamss = List(),
// tpt = TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Int)],
// preRhs = Literal(const = ( = 1))
// ),
// source = /def
// )
// )
本来欲しいのは後者だけだったのだけれど、他所の compilation unit で定義した x も引いてきてしまったが、とりあえず定義元のコードの構文木を取得することができた。
おまけ parser で遊んで見る
scala> import dotty.tools.dotc.core.Contexts._
scala> given Context = (new ContextBase).initialCtx
lazy val given_Context: dotty.tools.dotc.core.Contexts.Context
scala> import dotty.tools.dotc.parsing.Parsers
scala> import dotty.tools.dotc.util.SourceFile
scala> val parser = new Parsers.Parser(SourceFile.virtual("<meta>", "class X {} extends Base;"))
val parser: dotty.tools.dotc.parsing.Parsers.Parser = dotty.tools.dotc.parsing.Parsers$Parser@175db8b0
scala> parser.parse()
val res1: dotty.tools.dotc.ast.untpd.Tree = PackageDef(Ident(<empty>),List(TypeDef(X,Template(DefDef(<init>,List(),List(),TypeTree,EmptyTree),List(),ValDef(_,EmptyTree,EmptyTree),List(EmptyTree)))))