scruff、Scala コード・フォーマッター
scruff という高速に正規化を行う Scala のコード・フォーマッターを Rust で作ってリリースした。
設計方針: 正規化
Scruff は、Python のコードフォーマッターの Black と Ruff にインスパイヤされた正規化を行うコード・フォーマッターだ。これは Scruff をかけたコードは、どのような形から始めても同じになることを意味する。と言っても分かりづらいので、具体例で説明する:
// original input
object A:
val a = List(1, 2)
val b = List(
1,
2
)
val c =
List(1, 2)
デファクト・スタンダードな Scala フォーマッターである Scalafmt を使った場合、上記のコードはフォーマット後もそのままだ。一方 Scruff は以前のフォーマットを無視して、統一した空白文字を以下のように適用する:
// scruffed
object A:
val a = List(1, 2)
val b = List(1, 2)
val c = List(1, 2)
もし行が max-column (デフォルト: 100) を上回った場合は、Scruff は縦に分割する:
// original input
object A:
val a = List("scala-library-2.13.16.jar", "scala3-library_3-3.8.4.jar", "sbt-launch-2.0.0-RC2.jar")
は以下のようになる:
// scruffed
object A:
val a = List(
"scala-library-2.13.16.jar",
"scala3-library_3-3.8.4.jar",
"sbt-launch-2.0.0-RC2.jar",
)
設計方針: 高速
Scruff は、Tree-Sitter Scala を利用して Rust で書かれており、Scala コードのパースとフォーマットを、並列に、ネイティブコードで行う。未処理の scala/scala3 リポジトリは、207k sloc のコードがあるが、Scruff はこれを 1.50s でフォーマットする:
- Scruff (with 12 core): 1.50s (138.6 ksloc/s)
- Scalafmt (with 1 core): 8.82s (23.6 ksloc/s)
- Scalafmt (
scalafmtParallelism := 12): 1.93s (107.7 ksloc/s)
これは、設定をいじらない状態だと Scruff は Scalafmt に比べて約6x 速いことになるが、これはデフォルトの並列度の設定に依る所が大きい。興味深いことに、既にフォーマット済みのコードに再実行すると、Scruff は 0.60s で終わる。これは、ファイル出力が入力と同一の場合、Scruff は再パースと等価性の検査を省略しているためだ:
- Scruff (with 12 core): 0.60s (339.0 ksloc/s)
- Scalafmt (with 1 core): 8.09s (25.2 ksloc/s)
- Scalafmt (
scalafmtParallelism := 12): 1.81s (112.7 ksloc/s)
ということで、Scalafmt の設定次第で Scruff は 3~10 倍高速だということだ。もし、リポジトリ全体を 0.6秒でフォーマット化できるなら、いつでもフォーマット化するべきだろう。
設計方針: 定食 A か定食 B
Scruff 0.1.0 は、最小限の設定しか持たない:
# .scruff.toml
max-column = 100
dialect = "scala3"
exclude-paths = [
"glob:**/src/sbt-test/**",
]
dialect = "scala3"は中括弧省略構文、fewer braces、then構文などマキシマリストの Scala 3 に書き換えを行う。dialect = "scala213"とdialect = "scala213source3"は中括構文に書き換えを行う。
他にいじる設定は無し。
使い方
ネイティブのバイナリファイルは GitHub Releases https://github.com/eed3si9n/scruff/releases/tag/v0.1.0 に公開している。便宜のため、sbt プラグインも作って、scruffFmt、scruffFmtCheck、scruffFmtOnCompile をビルドに追加する。
// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-scruff" % "0.1.0")
scruffFmt を最初に使ったときにバイナリのダウンロードが行われる:
sbt:scruff-root> scruffFmt
[info] downloading scruff 0.1.0 for universal-apple-darwin
[info] 4 files left unchanged
[success] elapsed time: 1 s
コンパイル時に自動でフォーマットを行いたい場合は:
ThisBuild / scruffFmtOnCompile := true
フォーマット理論
Scruff はその理論的基礎を Philip Wadler 氏の 1997/1998 の論文 ‘A prettier printer’ (https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) に負っている。このペーパーの Scala 実装として typelevel/paiges があるので、コンセプトとして知っている人もいるかもしれない。
Wadler ペーパーは、Doc というデータ型を説明していて、これはコードをテキストの小さな「かたまり」を追加することによって記述できる。フォーマット化を行うときは、プリンターに max-column を渡して、改行やインデントは、プリンターが受け持つ。このプリンターは線形にスケールする。
実際には、パース時にコメントなど全部の空文字も保持する必要があるので、まずは内部で Tree と呼んでいるロスレスな具象構文木 (CST) を構築する。フォーマット化と書き換えは、この Tree を Doc に変換する作業となる。その後、フォーマット化されたコードは再パースして、処理されたコードが等価かの検査を行う。この等価性の検査は、implicit パラメータと using パラメータなど、書き換えで変わるものは無視する。
ご感想お待ちしております
できたばかりなので、パースやフォーマットなどまだバグもあるかもしれない。何か変な挙動が見つかったら報告してください。Tree-Sitter Scala は今まで構文のハイライト目的で使われることが多かったが、フォーマット化に使うことで何十ものパースの不具合が発見され、最新のリリースではそのいくつかの修正が既に入った。
AI 利用開示
Scruff は Claude Code を用いて開発した。コードの方向性は自分で細かく操作して、テストなども確認したが、実際の Rust のコードそのものの大半は LLM によって書かれた。