💻

Swiftコンパイラ開発入門

2024/12/19に公開

背景

SwiftコンパイラがOSSになってから随分と年月が経ち改善が進んだことで、コンパイラ開発に参加しやすくなってきています。一方で、開発チームが多忙なためなのか、issueを報告しても対応されない状況が続いています。そのため、Swiftユーザが自らパッチを提出する必要性が高まっています。

想定読者

そこでこの記事では、一般的なSwiftユーザの方向けに、Swiftコンパイラの開発を始めるためのノウハウや手順を解説します。こうすることで読者が自らパッチを提出できるようになり、Swiftコンパイラの改善が加速するでしょう。そして、全てのSwiftユーザがその恩恵を受けることでしょう。

なお、マシンはほどほどのスペックのmacを前提とします。また、Apple Siliconを推奨し、記事中ではそれを前提とします。Intelの方は適宜読み替えてください。

手元の環境構築

まずは手元のマシンの環境を整えましょう。
基本的にマニュアルに書いてあるんですが、改めて説明します。

まずは必要なソフトウェアをインストールします。

Xcode

Xcodeをインストールしてください。基本的には最新の安定版が望ましいです。ごく稀に最新のBeta版が必要な時期もあります。マニュアルによると、CIのウェブページに要求バージョンが書いてあるらしいです。

マシンに複数のバージョンのXcodeをインストールしている人は、CLI環境で使用するXcodeのバージョンを指定する設定を忘れないように気をつけましょう。
メニューバーの Xcode > Settings... から設定ダイアログを開き、 Locations ページの中の Locations タブの中の Command Line Tools: のセレクトボックスから設定できます。

Homebrew

必要な他のツールを入れるためにHomebrewを入れましょう。もしこれまで使ったことがない人は、セットアップ手順におけるパスを通す手順もちゃんとやっておきましょう。

Git

Gitを入れましょう。mac同梱のやつで大丈夫だと思いますが、Homebrewで新しいのを入れておくと無難だと思います。

brew install git

CMake

CMakeはクロスプラットフォームでメタなビルドツールです。CMakeでプロジェクトを構成する事によって、主にC++のプロジェクトを様々なOSでビルドできるようになります。CMakeでビルドすることによって、macOSではXcodeのプロジェクト、WindowsではVisual StudioのプロジェクトのようにプラットフォームネイティブなIDEのプロジェクトを生成したり、LinuxではMakefileやNinjaなどCLIのビルド構成を出力してくれます。その後、それらのツールを使って実際のC++プロジェクトのビルドをします。ただまあ後述しますが、Ninjaの生成以外は気にしなくて良いです。

Homebrewで新しいのを入れましょう。

brew install cmake

Ninja

Ninjaは高速なビルドツールです。Swiftコンパイラ開発では主にこれでビルドをします。先述のCMakeがこのNinja向けのビルド構成を出力します。

Homebrewで新しいのを入れましょう。

brew install ninja

Python

Swiftコンパイラプロジェクトにおけるスクリプト言語としてはPythonが採用されていて、チェックアウトやビルドするにもこれが必要です。先述のCMakeはこういったPythonスクリプトによって起動されます。
macOSに同梱されているもので大丈夫な気もしますが、バージョンの問題が出るかもしれないので新しい方が無難です。PythonのインストールについてはHomebrewに任せるのが簡単だと思います。

brew install python

Flake8

Flake8はPythonのLinterです。自動テストに組み込まれているのでインストールしておいた方が良いです。Pythonの標準のパッケージマネージャであるpipを使ってインストールします。

python3 -m pip install --break-system-packages flake8 flake8-import-order

ただ、 --break-system-packages については自己責任で指定してください。


ここまででマシン環境の準備は完了です。

チェックアウト

マシン環境が整ったらいよいよソースコードを取得しましょう。

プロジェクトディレクトリの用意

ソースコードをチェックアウトする前に、ディレクトリの準備をしましょう。Swiftコンパイラプロジェクトは、複数のgitリポジトリが同一のディレクトリにフラットに並びます。まずはこれらを展開するための空のディレクトリを作りましょう。

mkdir swiftlang

プロジェクトディレクトリの名前は好きなもので良いですが、以降の説明では swiftlang だと仮定します。

Swiftリポジトリのチェックアウト

まずはSwiftリポジトリをチェックアウトします。
先ほど用意したディレクトリに普通にgitでチェックアウトすれば良いです。

cd swiftlang
git clone https://github.com/swiftlang/swift

ここでものすごく時間がかかります。休憩しましょう。

依存先リポジトリのチェックアウト

Swiftリポジトリには複数の依存先リポジトリがあります。これらはgit submoduleのような標準的な仕組みではなく、Swiftリポジトリに含まれている専用のスクリプトを使って管理します。

これらはプロジェクトディレクトリに展開され、swiftディレクトリと横並びになります。

以下のコマンドでチェックアウトします。

cd swiftlang/swift
utils/update-checkout --clone

--clone オプションは、まだチェックアウトしたことがない依存先リポジトリを新規に取得することを指示します。よって、今回のように初めて実行するときには必須です。今後開発を継続する中で、依存先をアップデートするだけで良い時には不要です。ただ、依存先リポジトリはそれなりの頻度で追加されてきており、それを見逃して取得もれを起こすとトラブルが起きたりするので、常に指定するのが無難です。

この手順もまあまあ時間がかかったと思います。

一見これでチェックアウトは終わりですが、まだ重要な工程が続きます。

安定バージョンを探す

ここまでの手順で最新版がチェックアウトできています。しかし、最先端のSwiftプロジェクトはしょっちゅう壊れています。Swift本体リポジトリの特定のコミットと、依存先リポジトリ達のコミットの対応関係は固定されていないので、当然のように壊れます。よって、あなたがチェックアウトしたそのバージョンはおそらくビルドすることができません。なのでまずはビルドできる安定バージョンを探しましょう。

SwiftプロジェクトはCIが毎日ビルドしていて、成功するとスナップショットタグが打たれます。これは依存先リポジトリ全てに打たれるので、コミットの組みも固定してくれます。

スナップショットタグは swift-DEVELOPMENT-SNAPSHOT-yyyy-MM-dd-a のような形式をしています。a の部分は同日に複数回出荷される場合に b, c とインクリメントする桁だと思われますが、僕は a しかみたことありません。

これを、gitコマンドでタグ一覧を取得して、できるだけ最新の日付のものを探しましょう。

git tag -l

ちなみにそれを見るとわかりますが、2週間程度ずっとスナップショットが出ていないこともざらにあります。

また、紛らわしいものとして swift-6.1-DEVELOPMENT-SNAPSHOT-yyyy-MM-dd-a のような言語バージョン付きのタグもあるので注意しましょう。これはバージョンのリリース計画ごとにカットされたブランチごとのスナップショットですが、それゆえに最新の変更は取り込まれていないので通常のコンパイラ開発時には必要ありません。

チェックアウトするタグが決まったら次に進みます。
以降の説明では例として swift-DEVELOPMENT-SNAPSHOT-2024-12-13-a を選択したことにします。

スナップショットをチェックアウトする

スナップショットをチェックアウトするには、先述のスクリプトを再び使用します。これはSwiftリポジトリ自体も更新します。

cd swiftlang/swift
utils/update-checkout --clone --tag swift-DEVELOPMENT-SNAPSHOT-2024-12-13-a

--tag オプションによって先ほど選択したスナップショットタグを指定します。

これでチェックアウトは完了です。

ビルド

いよいよビルドです。

ビルドをするには専用のスクリプトを実行します。すると、それがCMakeを実行してNinjaのビルド構成が生成されます。続いてNinjaが実行されることでビルドが実行されます。

以下のコマンドでビルドが開始します。

cd swiftlang/swift
utils/build-script --skip-build-benchmarks \
  --swift-darwin-supported-archs "$(uname -m)" \
  --release-debuginfo --swift-disable-dead-stripping \
  --bootstrapping=hosttools

これにはとても長い時間がかかります。CPUが全力を出し、マシンは温まってファンが音を立てて回ります。楽しいですね。

コマンドはオプションが多くて難しいですが、ここに挙げたのはマニュアルに書いてあるものです。基本的にこれに従っておけば十分で、覚える必要もないのでコピペしてください。

ただこの中でも気にしておくと良い重要な部分は --release-debuginfo の指定です。これはSwiftコンパイラ自体をコンパイルする時のコンパイルモードを指定していて、その値は デバッグ情報付きのリリースビルド という意味です。トゲナシトゲトゲ感がありますが、要するに最適化を有効にするので高速に動作するが、コンパイラの実行時にデバッガを繋げてデバッグすることもできるという、デバッグモードとリリースモードの間の良いところ取りのようなモードです。このモードの名前は他の場所では RelWithDebInfo と表記されることもあります。

なお、良いところ取りではありますが、やはり最適化があるためにデバッガがうまく機能しないこともあります。最もデバッグに特化させるのであれば --debug を代わりに指定して、純粋なデバッグモードにすることもできます。その代わり動作が遅くなります。

また、Swiftコンパイラのソフトウェアスタックのうち、LLVM部分だけはリリースモードでビルドするとか、他にもいろいろコントロールするオプションが豊富にあります。興味を持ったら -h オプションを指定してヘルプを見てみると良いでしょう。

ビルドが終わると、プロジェクトディレクトリに build ディレクトリができます。
この中は以下のような構造になっています。

cd swiftlang
tree -L 2 build
build
└── Ninja-RelWithDebInfoAssert
    ├── cmark-macosx-arm64
    ├── earlyswiftdriver-macosx-arm64
    ├── llvm-macosx-arm64
    └── swift-macosx-arm64

1段目のディレクトリは、指定したビルドツールとコンパイルモードを表しています。この例なら、ビルドツールはNinjaで、デバッグ情報付きのリリースモードだったという事です。これらの指定が変わると別のディレクトリになるので、ビルドの中間ファイルやキャッシュなどが分離され、適宜キャッシュとして再利用されるようになっています。この1段目の部分が実質的な ビルドディレクトリ で、今後様々な場面で出てきます。ルートの build ディレクトリは役割から言うと ビルドディレクトリ置き場ディレクトリ のようなものです。

2段目のディレクトリはサブプロジェクトを分割しています。また、CPUアーキテクチャが付与されています。Swiftを配布する際のツールチェーンパッケージにはSwiftコンパイラ以外にも、CコンパイラであるClangなども含まれています。ClangなどはLLVMプロジェクトに由来するので llvm-macos-arm64 ディレクトリの中に生成されていて、SwiftコンパイラなどSwiftプロジェクトの生成物は swift-macosx-arm64 ディレクトリの中に含まれているといった具合です。Intelマシンだとちょっと違う名前になるでしょう。

もしビルドに失敗したら

もしかしたら運悪くビルドに失敗したかもしれません。ちゃんとセットアップしたつもりでも、手元のマシンの環境に問題があったりしてそういうこともあります。macOS自体のバージョンが相性が悪いようなこともたまにあります。

そういう場合はまず上述の build ディレクトリを削除します。そして安定バージョンを探してチェックアウトするところからやり直すのが良いです。思いきって1ヶ月ぐらい古いバージョンを試してみると良いでしょう。

もし手元環境に不安があって、できるだけ安定度の高いバージョンを見つけたい場合は、デイリースナップショットではなくリリースタグを選ぶと良いです。リリースタグは一般向けに出荷されたバージョンについているタグで、 swift-6.0.3-RELEASE のような形式をしています。

Xcodeによる作業

Xcodeプロジェクトの生成

ビルドが無事に成功したら、次にコード編集をする環境を整えましょう。SwiftコンパイラはXcodeを使って開発することができます。

上の方でCMakeはNinjaだけでなくXcodeのプロジェクトを出力できる事に触れました。少し前まではこの機能を使うのが良い方法でした。しかし、最近全く新しい方法が登場し、CMakeを使う必要はなくなりました。それはNinjaのビルド構成に基づいてプロジェクトを生成する専用のツールで、swift-xcodegen と呼びます。以下のPRで2024年11月に導入されました。

https://github.com/swiftlang/swift/pull/77406

swift-xcodegen は単体のSwiftパッケージとして実装されていて、Swiftで書かれているので、中身を見てみるのも良いでしょう。新しいので今も活発に開発が行われています。

さて、プロジェクトを生成するには以下のコマンドを実行します。これはマニュアルにも書いてあります。

cd swiftlang/swift
utils/generate-xcode ../build/Ninja-RelWithDebInfoAssert

コマンドの引数として、ビルドディレクトリを指定します。

生成されたXcodeのプロジェクトはプロジェクトディレクトリに Swift.xcodeproj という名前で配置されます。早速開いてみましょう。

cd swiftlang
open Swift.xcodeproj

Xcodeプロジェクトの構成

Xcodeプロジェクトを開くと以下のような見た目をしています。

左のファイルツリーは実際のファイルツリーにおおよそ対応しています。ターゲットはたくさんあります。また、スキームもたくさん生成されています。スキームはXcodeの画面最上部の中央付近にあるバーの左端の部分です。

作業する時は用意されているスキームのいずれかを選択し、Xcodeの通常の操作でビルドしたり実行したりします。いわゆるコンパイラ本体はこの中の swift-frontend スキームです。ひとまずこれを選択して、⌘ + B でビルドしてみると良いでしょう。内部的には元になったNinjaを呼び出すようになっているので、うまくいけばビルドキャッシュが効いてすぐにビルド完了するはずです。

なお、コンパイラ本体は swift-frontend ですが、普段利用している swift コマンドはこれとは違います。swift コマンドの正体は swift-driver です。ドライバはコマンドラインを受け取ってそれをフロントエンドコマンドの呼び出しに変換します。入力によってはドライバは複数回のフロントエンドを呼び出します。つまり、コンパイラとしての機能を持っているのはフロントエンドなので、コンパイラ開発において主に変更するのはそれの挙動ということです。

作業ディレクトリの用意

コンパイラ開発においては、コンパイラに入力としてソースコードを投げ込む必要があるので、作業ディレクトリを切っておくのが良いでしょう。
プロジェクトディレクトリに work ディレクトリを作成しましょう。

cd swiftlang
mkdir work

ソースコードをおいておきましょう。

cd work
cat > a.swift << EOF
print("hello")
EOF

実行の設定

swift-frontend が作業ディレクトリ上で実行できるように設定しましょう。Xcodeのスキームのメニューから Edit Scheme... を選択します。

ダイアログの左ペインから Run ページを選択し、Options タブを開きます。その中の Working Directory の項目を編集します。 Use custom working directory: のチェックボックスをオンにして、その下のテキストフィールドに作業ディレクトリのフルパスを指定します。

次に Arguments タブを開きます。その中の Arguments Passed On Launch の項目に -dump-ast a.swift と設定します。

ダイアログを閉じて設定を完了したら、 ⌘ + R で実行しましょう。
XcodeのコンソールにASTが出力されたらうまく動作しています。

-dump-ast はフロントエンドの動作モードの一つで、ソースコードをパースし、型チェックを行い、その結果を出力します。

デバッグしてみる

Xcodeでデバッグしてみましょう。
TypeChecker.cppevaluator::SideEffect TypeCheckSourceFileRequest::evaluate(Evaluator &eval, SourceFile *SF) const 関数の冒頭部分にブレークポイントを張ってください。

実行してみましょう。-dump-ast は型チェックをするのでここに引っ掛かります。

ブレークし、スタックトレースや変数ビューワが動作します。

LLDBを使ってみましょう。コンソールに e SF->dump() を入力してください。
dump メソッドが実行され、コンソールにソースファイルのASTが表示されます。

これから型チェックをするファイルなので、まだ型が解決できていない事が確認できますね。

Swiftコンパイラのコードでは、多くの型に dump メソッドが実装されていて、このようにデバッグ時に利用すると便利です。

基本的な開発作業の方法を説明しました。

開発中の運用

コード編集やデバッグ以外にも様々な操作が必要になるでしょう。その方法を説明します。

ファイルの追加・削除

ソースファイルの追加・削除はまあまあ大変です。Xcodeプロジェクト上で操作しても多分うまくいきません。C++ソースについては、CMakeの定義ファイルである CMakeLists.txt を編集する必要があります。編集したら、CMakeの再実行が必要なので、 utils/build-script を改めて実行します。さらに utils/generate-xcode も再実行します。いろいろと面倒なのでできるだけ避けた方が良いでしょう。

リビルド

リビルドにはいくつかの手段があります。まず、Xcode上で通常通りビルドしても良いです。utils/build-script を再実行するのも良いです。ファイル構成に変化がない場合は、以下のようにCLIからNinjaを直接実行することでもリビルドできます。

cd swiftlang/swift
ninja -C ../build/Ninja-RelWithDebInfoAssert/swift-macosx-arm64 bin/swift-frontend

-C オプションを指定し、続いてビルドディレクトリの中のプロジェクトディレクトリまで指定します。その後スペースを開けて、ビルド生成物を指定します。Ninjaが定義に基づいて依存解決を行い、必要なビルドが行われます。

ちなみに、Xcodeプロジェクトの ninja-build-swift-frontend ターゲットの Build Phases 設定の中をみると、 Run Script として上記のNinjaのコマンドを実行するように記述されています。

クリーン

リビルドがうまくいかなくなった場合などはクリーンしてやり直しましょう。
クリーンの方法として、 utils/build-script--clean オプションを指定することができます。その場合、その他のオプションは全く同じように設定します。

ただ、もっと確実で簡単な方法として、ビルドディレクトリをファイルシステム上で削除しても良いです。ビルドの中間ファイルは全てそこにまとまっているので綺麗になります。

テスト

コード編集が上手くいったら、既存の機能が壊れていないか確認するために自動テストをしましょう。
これは現状Xcodeプロジェクトからはできないので、CLIで行います。

テストケースは test ディレクトリの中に書かれた個別のSwiftソースファイルとして記述されています。

テストの実行は専用のスクリプトを使うのが便利です。以下のように利用します。

cd swiftlang/swift
utils/run-test --build-dir ../build/Ninja-RelWithDebInfoAssert test/Parse

--build-dir オプションでビルドディレクトリを指定し、その次に実行するテストケースを指定します。ここでは test/Parse ディレクトリを指定したので、その配下のテストケース全てが実行されます。この例はマニュアルにも書かれています。

さらに細かく個別のファイルを指定することもできるし、逆に test だけを指定して全て実行することもできます。パッチの提出前にはこれをやると良いでしょう。

また、 validation-test というディレクトリもあります。これはより幅広く様々な動作を確認するためのテストスイートです。test と同じように run-test スクリプトに指定できます。実行にはより多くの時間がかかりますが、変更内容の影響範囲が測りきれない場合は実行した方が良いでしょう。

また注意点として、パッチを完成させてからテストを実行するのではなく、作業に取り掛かる前に一度テストを実行しておいた方が良いかもしれません。たとえビルドに成功する状況であっても、手元ではテストが全て通らないということがあるからです。その場合、元から失敗しているテストはパッチ適用後に失敗しても心配は少ないですが、元から失敗していることを把握していなかった場合は面倒なことになるからです。

パッチの提出

作業が終わったら提出しましょう。一般的なGitHub上のOSSのお作法に則っていれば大丈夫です。英作文が一番のハードルですが、僕は最近はChatGPTを使うことで楽になりました。

CI

レビューとCIが通ればメンテナにマージしてもらえますが、このCIもまた特殊です。
GitHubのプルリクエストのコメント欄でbotにメンションする事でCIがキックできるのですが、これはコミッター認定されたユーザにしか権限がありません。初心者コントリビュータの場合はCIを起動できないので、メンテナが対応してくれるのを待つか、コミッターの友人にお願いしましょう。

mainブランチはビルドができない事が多いと書きましたが、同じ理由でCIが通らない事もよくあります。CIはSwift本体リポジトリ以外の依存先リポジトリについては単に最新環境をチェックアウトするからです。

基本的には Ubuntu, macOS, Windows の3つの環境で通る必要があります。これらのステータスページを見てみれば、結構失敗しているのがわかります。

ですから、もしパッチを提出してからCIでエラーが出ても、落ち着いてログを見に行きましょう。自分の変更が関係ない事もよくあります。ステータスページを見て、成功が多くて調子がよさそうなときに改めてキックしましょう。

コミッターを目指そう

良いパッチが5件ほど通っていればコミッター認定を申請できます。最初はこれを目指すのも良いでしょう。

取り組むべきテーマ

ここまでコンパイラ開発に参加する方法を説明してきました。ですがそもそもどのようなテーマに取り組めば良いか、説明しておきましょう。

Swift言語へのいきなりの変更は認められない

まず最も重要な事として、Swift言語自体の機能追加や仕様変更は認められません。こうした変更はSwift Evolution というプロセスを通すことになっていて、プロポーザル書類の提出やフォーラムでの審査が必要です。そのため、こうした変更を提出しても直接マージされることはないので注意しましょう。

バグ修正

わかりやすいのはコンパイラのバグ修正です。7000件ほどのissueが溜まっているので、興味のあるバグを探して直しましょう。

稀に言語の仕様変更とバグ修正のどちらに該当するか微妙なケースもあります。そう言う時は、とりあえずバグ修正としてパッチを投げるとレビューの過程で意見交換がしやすいです。

コンパイラ開発者体験の改善

ここまで見てきたように、Swiftリポジトリにはコンパイラソース以外にも、コンパイラ開発のためのツールやスクリプトが多く含まれています。開発していくうちに課題をみつけたら、それを改善してみるのも良いでしょう。

コード品質向上

バグ修正や動作変更はなくとも、コードの品質を改善するのも良いでしょう。コードベースは巨大であり開発ペースも高速なので、必ずしも最高の仕上がりではない部分もみつかったりします。なので、とりあえず好きな部分のコードを読んでみるのは良いアプローチでしょう。

性能向上

コンパイラの動作性能を向上させるのも良いでしょう。ただ、この点については既存のコードはかなり高いレベルで書かれています。改善点を見つけるのは難しいかもしれません。

よくある悩み

最後によくある悩みについて触れます。

C++がわからない

C++がわからないから取り組めないと思うかもしれません。ですが最近はコンパイラソースの中でも一部がSwiftで書かれるようになってきています。そういう部分にアプローチすると良いかもしれません。

コンパイラがわからない

とはいえコンパイラについて知識がないので何もわからないと思うかもしれません。それならSwiftコンパイラの勉強会に参加してみるといいでしょう。そこで詳しい人に相談すると良いでしょう。

Discussion