Swift コンパイラのアーキテクチャ

  • 66
    いいね
  • 0
    コメント

Swift のコンパイラの開発に手を出してみたい方向けに、コンパイラの全体構成がどうなっているのかを、大まかに説明します。コードリーディングの参考になればいいなと。

Swift コンパイラの構成

僕はプログラムを把握するときに、エントリポイント(最初に実行される箇所、 main 関数)が分からないとすごく不安になります。逆に言えば、エントリポイントさえ分かれば、そこから処理を追っていけば良いのでその後の理解が非常に楽になります。なので最初は swift コマンドのエントリポイントから。

注意: このエントリで、 GitHub にある実際のコードにリンクを張っていますが、行番号は時と共に変わってしまうので、ファイルまでのリンクになっています。関数名などで検索してください。

ドライバー (Driver)

swift コマンドですが、実体は tools/driver です。
tools/driver/driver.cppmain 関数があり、ここから lib ディレクトリ で実装されている各種ライブラリを呼び出していくようになります。

ドライバにはコマンドライン引数などによって、いくつかのモードがあります。

サブコマンドランチャー

swift package と呼び出されたら swift の実行ファイルと同ディレクトリにある swift-package を起動するだけのランチャーです。swift コマンドへの第一引数が下記の条件にすべて合致したときに実行されます。

  • - から始まらない
  • . を含まない (ファイル名ではない)
  • repl ではない1

試しに swift hogehoge みたいなコマンドを実行すると swift-hogehoge が見つからない的なエラーが表示されるはずです。

バッチモード

swiftc コマンドです。 swiftcswift のシンボリックリンクになっていて、 argv[0]"swiftc" のときにバッチモードになります。ソースファイルから実行可能ファイルを作るには、コンパイルやリンクなど複数のステップを踏む必要がありますが、このモードはその一連の動作を構築し、バッチ処理する形で実行します。

hello.swift
func hello() {
  print("Hello Swift!")
}
main.swift
hello()

-v オプションをつけるとサブプロセス呼び出しが行われていることが見えます。

実行するサブプロセス表示しながらコンパイル
$ swiftc -v -o hello main.swift hello.swift
Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42)
Target: x86_64-apple-macosx10.9
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -primary-file main.swift hello.swift -target x86_64-apple-macosx10.9 -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -color-diagnostics -module-name hello -o /var/folders/y0/845byh512k53x98tw3ch5j2w0000gn/T/main-4e5977.o
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c main.swift -primary-file hello.swift -target x86_64-apple-macosx10.9 -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -color-diagnostics -module-name hello -o /var/folders/y0/845byh512k53x98tw3ch5j2w0000gn/T/hello-6eeba6.o
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld /var/folders/y0/845byh512k53x98tw3ch5j2w0000gn/T/main-4e5977.o /var/folders/y0/845byh512k53x98tw3ch5j2w0000gn/T/hello-6eeba6.o -force_load /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_macosx.a -framework CoreFoundation -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk -lobjc -lSystem -arch x86_64 -L /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -rpath /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -macosx_version_min 10.9.0 -no_objc_category_merging -o hello
  1. ソースファイル毎に swift -frontend -c を呼び出して .o ファイルを作る
  2. リンカである ld.o をリンクし、実行ファイルを作る

という感じですね。 -frontend は後述のフロントエンドモードです。

実行せずに、コマンドだけを表示したいなら -driver-print-jobs です。

実行するコマンド一覧(macOS)
$ swiftc -driver-print-jobs -o hello main.swift hello.swift

フロントエンドモード

Swiftコンパイラの核です。swift -frontend <action> [options...] <inputs...> で呼び出されます。Clang における -cc1 みたいなもので、ザックリ言えば .swift ファイルを読み込んで .o ファイルに変換するところまでを担当します。コンパイルの本体ですね。前述の通りバッチモード( swiftc ) でのコンパイルも、実際にはフロントエンドモードをサブプロセスとして実行しています。フロントエンドの中でもいろいろモード(アクション)があるのですが、フルで実行するのは -c(-emit-object) です。

emit-object
$ swift -frontend -emit-object -o test.o test.swift

次節では、このアクションを実行されたときの処理のフローを説明していきます。

注意:フロントエンドモードはドライバモードを通して起動されるのが通常であり、普通の Swift プログラマが直接 -frontend を指定して起動することは想定されていません。コマンドラインオプションも結構な頻度で変わります。

フロントエンド(またはそのサブシステム)は、コンパイルの他にも SourceKit から使用されてコード補完をしたり、シンタックスカラーリング等にも利用されます。

即実行モード

Swiftのソースファイルをそのまま実行するモードです。Immediateモードと呼ばれます。
その実は swift -frontend -interpret <オプションいろいろ> <入力ファイル>を起動しているだけです。

Swiftスクリプト実行
$ swift hello.swift

また標準入力にソースを流して実行することもできます。その場合は入力ファイル名を - にします。

標準入力実行
$ echo 'print("Hello Swift!")' | swift -
Hello Swift!

REPLモード

入力ファイル名を指定せずに swift コマンドを実行すると REPL モードになります。

REPL
$ swift
Welcome to Apple Swift version 4.0 (swiftlang-900.0.49.1 clang-900.0.29). Type :help for assistance.
  1>  

lldb コマンドが特定のディレクトリに存在する場合 lldb を Swift の REPL モードで実行、そうでなければ Frontend 組み込みの REPL を実行します2

また標準入力にソースを流して実行することもできます。入力ファイル名を - も含めて一切指定せず、標準入力にソースを流すと起動します。

標準入力をREPL実行
$ echo 'print("Hello Swift!")' | swift
Welcome to Apple Swift version 4.0 (swiftlang-900.0.49.1 clang-900.0.29). Type :help for assistance.
Hello Swift!

どうして Immediate モードと別になっているのかよく分からないですが、標準入力は REPLへの入力となるので、REPLのコマンドも効きます。

$ echo ':help' | swift
Welcome to Apple Swift version 4.0 (swiftlang-900.0.49.1 clang-900.0.29). Type :help for assistance.

The REPL (Read-Eval-Print-Loop) acts like an interpreter.  Valid statements,
expressions, and declarations are immediately compiled and executed.
The complete set of LLDB debugging commands are also available as described
below.  Commands must be prefixed with a colon at the REPL prompt (:quit for
example.)  Typing just a colon followed by return will switch to the LLDB
prompt.
Debugger commands:
  apropos           -- List debugger commands related to a word or subject.
  breakpoint        -- Commands for operating on breakpoints (see 'help b' for
... 略

その他モード

他にもいくつかあります。そういうのが有るというのだけ知っておけば。

起動方法
swift-format Swift ソースファイルの自動整形
swift-autolink-extract コンパイルした .o からリンカ用のフラグを生成3
swift -modulewrap .swiftmodule のデータをそのまま .o4
swift -apinotes .apinotesファイル(C API マッピングヒント) の YAML ←→ バイナリ変換

各モードがコマンドラインオプション指定だったり、シンボリックリンクによるコマンド名変更だったりするのはどういう基準なんでしょうね。

フロントエンド (Frontend) の構成

lib/FrontendTool/FrontendTool.cpp にある swift::performFrontend がエントリポイントになります。大きな流れは下記の通りです。

  1. フロントエンドの起動設定を保持するオブジェクトである CompilerInvocation を コマンドラインオプションなどから生成する。
  2. CompilerInstance を生成し、CompilerInvocation によってそのセットアップを行う。
  3. .cppperformCompile にてコンパイル実行。ここで下図パイプラインが走ります。各コンポーネントにおいてエラーを検知した場合にはそこで処理が終了します。

SwiftCompiler.png

CompilerInstance

include/swift/Frontend/Frontend.h で定義されています。

図には示していませんが、コンパイル全体の状態や実行を管理する、フロントエンドにおいて重要な部分です。

CompilerInstance は下記のような、コンパイル中の主要なシングルトンのオーナーの役割を担っています。

  • SourceManager : ソース管理
  • DiagnosticEngine : 診断エンジン
  • ASTContext : ASTのメモリ管理、外部モジュールの読み込みなど
  • ModuleDecl: コンパイル中の ASTモジュール
  • SILModule : SILモジュール

パイプライン中の Parse および Sema に関しては、このオブジェクトの
CompilerInstance::performSema メソッドを通して実行されます。入力ファイルが複数の場合は、一回の performSema で、全ての入力ファイルの Parse と Sema を完了させてから、次のフェーズに進みます。

ASTContext

もう一つ重要な要素に ASTContext という物があります。

include/swift/AST/ASTContext.h で定義されています。

CompilerInstance によってインスタンス化され、LLVM IRの生成が完了するまでずっと生き続ける、非常にライフタイムが長いオブジェクトです。 AST ノードのメモリ管理がメインの仕事なのですが、その他にも

  • stdlib含め、外部モジュールの読み込みとそのオーナー
  • コンパイラ既知の stdlib APIの管理

などの役割を担っており、また、フロントエンドの各サブシステムは実行中に FrontendToolCompilerInstance のことは意識しませんが、ASTContext はフロントエンドのほぼ全てのサブシステムから参照されます。

また SourceManagerDiagnosticEngine の参照を持っており、各サブシステムからこれらを利用する際の窓口ともなっています。全体から見えて欲しいものはとりあえずここに参照を突っ込んどけ、的なやつです。

サブシステム

コンパイルのパイプライン上に現れる、主要なプロセッサです。 swift.org にも説明があります。

正しい用語かどうかはわかりませんが、 ソース上では "subsystems" という書かれ方をされているのでここでは「サブシステム」と呼びます。swift.org では components と表記されています。

Parse

ソースを AST(抽象構文木) に変換します。割とシンプルな 再帰下降構文解析器(Recursive Descent Parser) で、 lib/Parse/Lexer.cpp によって字句解析 (Lexical Analysis) を行いつつ、Lexer から得たトークンの並びを判断して AST を構築していきます。型情報や、意味解析には関知しません5。例外的に、ローカル変数の使用など、文脈上明らかな部分だけこのタイミングで名前束縛 (Name Binding) が行われます。また文法エラーや文法に関するワーニングを発行するのも Parse の仕事です。
エントリポイントは lib/ParseSIL/ParseSIL.cppswift::parseIntoSourceFile で、そこで lib/Parse で実装されている Parser がインスタンス化され、lib/Parse/ParseDecl.cppParser::parseTopLevel が呼ばれます。

Sema

意味解析(Semantic Analysis) フェーズです。Parseで生成された AST を受け取って、型チェック(Type checking)された AST に変換します。使用している変数の定義が見つからないなど、解析時に問題が発生した場合はエラーやワーニングを発行します。このフェーズが成功すると、完全な type-checked AST が生成され、そこからコードジェネレートできることが保証されます。

エントリポイントは lib/Sema/TypeChecker.cppswift::performTypeChecking です。
その前段に lib/Sema/NameBinding.cppswift::performNameBinding が実行されます。(歴史的に)この関数名ですが、実際にやっていることは import 文に従って外部モジュールを読み込むだけです。

また、 expression や pattern の型推論器として Sema の中にConstraintSystem というコンポーネントがあります。各 expression の型チェック毎にインスタンス化され、型推論をします。

docs/TypeChecker.rst を一部抜粋して意訳すると:

Swiftは伝統的な Hindley–Milner 型システム を彷彿とさせる「制約」を基にしたタイプチェッカーを使用し、双方向の型推論を実装している。この宣言的な制約システムの採用は、実際の型解決の実装を気にせず、言語中での意味を率直に表現することを可能とする。

Swift言語には、( Generics などの)多態型や、オーバーロードなど、いくつか Hindley–Milner 型システムには無い機能があり、実装がある程度複雑になっている。その一方、一回の型推論の範囲を一つの式(expression)または文(statement)に限定することにより、パフォーマンスを確保し、またエラー診断をより的確に行うことができる。

SILGen

Swift 独自の中間言語 である SIL (Swift Intermediate Language) を生成します。 SIL はプログラムの フロー解析 に適した言語で、 Swift の型情報などを利用した高レベルの最適化に適しています。 このフェーズでは、タイプチェック済みの AST を raw SIL と呼ばれる SIL に変換します。ここでも診断エラーが発生することがあります。

エントリポイントは lib/SILGen/SILGen.cppswift::performSILGeneration です。

SILOptimizer

このフェーズでは、SILGen で生成された raw SIL のデータフロー解析を行い、例えば未初期化の変数を使用しようとする等のプログラムエラーを検知します。結果的に canonical SIL と呼ばれる SIL を生成し、必要に応じて最適化を行います。また、このフェーズでエラー解析はすべて終了し、これ以降のフェーズではユーザーに対するエラー報告はしません。もしこれ以降に予期しない状態が発生したらコンパイラはクラッシュします。

最適化オプション (-Onone-O など) によって処理が異なります。最適化する場合には、ARC の最適化や、インライン化、Generics の特殊化など Swift 独自の型情報などを利用した最適化がこのフェーズで行われます。

エントリポイントは lib/SILOptimizer/PassManager/Passes.cpp で、最適化オプションにより:

  • -Onone
    • swift::runSILPassesForOnone
  • -O, -Ounchecked
    • swift::runSILOptPreparePasses ; した後に
    • swift::runSILOptimizationPasses

となります。また、IRGen の直前に swift::runSILLoweringPasses も実行されます。

run***Passes という名称から分かるとおり、この中で Pass パイプラインが構成され、実行されます。

IRGen

SwiftではバックエンドとしてLLVMを使用しているので、LLVM が処理できるように Swift の SIL を LLVM の IR に変換します。この時点で Swift の型情報は失われ、LLVMがサポートするプリミティブな型と関数のみの世界になります。

エントリポイントは lib/IRGen/IRGen.cppswift::performIRGeneration です。

LLVM

LLVM です。 IR に必要に応じて最適化を行い、( -target オプションなどで)指定されたアーキテクチャ向けの .o オブジェクトファイルを出力します。いわゆるバックエンドです。

エントリポイントは lib/IRGen/IRGen.cppswift::performLLVM です。基本的にはライブラリとしての LLVM を呼び出しているだけです。

モジュール関連

メインのパイプラインは上記の通りなのですが、その他に外部モジュール関連の登場人物がいます。 import で指定されたモジュールを読み込んだり、逆に他のモジュールから import できるようにするための機構です。

ClangImporter

Clang モジュール (C か Objective-C)を読み込んで、 Swift の API にマッピングし、Swift から使用できるようにします。AST を生成します。これによって、 Sema が C の APIを参照できるようになります。

  • 関数、クラスおよびメソッド、プロトコルなどの宣言
  • struct 定義
  • enum の定義
  • typedef の定義
  • 定数マクロ

などが Swift の AST として構築されます。当然関数などは最終的には C, ObjC の ABI(呼出規約) でコールする必要があるので、その旨の情報も AST 上に保持されます。

SerializedModuleLoader

Swiftのモジュールファイルである .swiftmodule, .swiftdoc を読みます。このファイルにはモジュールの API 宣言が保存されており、Sema が外部モジュールの API を参照できるようにします。 ClangImporter の Swift 版。

SerializedSILLoader

.swiftmodule には SILの情報も含まれており、これを読み込みます。標準ライブラリなど、モジュールによっては全てのSILの情報が入っており、その情報をインライン化することで高レベルな最適化が可能になったりします。

Serializer, SILSerializer

現在コンパイル中のプログラムを外部モジュールとして使用できるように .swiftmodule, .swiftdoc を書き出します。

PrintAsObjC

モジュールとは少し違いますが、Swift の API を Objective-C から使用できるようにするための仕組みです。ASTを受け取って、その中の Objective-C 互換の宣言 (@objc) を Objective-C のヘッダーファイル(.h) の形で出力します。 Xcode で Objective-C/Swift 混在のプロジェクトを扱っているときに、 Objective-C から #import するアレです。

エンティティ

ここでいう「エンティティ」とは、各コンポーネント間で受け渡しされるデータの事を指します。正式な用語は知りませぬ。

AST (抽象構文木)

include/swift/AST

Parse が作って Sema が加工して、SILGen が消費します。基本的には ソースの文字列をツリー構造に変換したものです。ModuleDecl がルートのオブジェクトで、ASTContext によってメモリ管理されます。

func foo(x: Int, y: String) -> Bar {
   return Bar(a: x, b: y)
}

であれば Parse は(正確ではないですが)だいたいこのようなツリーを生成します。

AST.png

SIL (Swift Intermediate Language)

include/swift/SIL

SIL は Swift が最適化やフロー診断を容易にするために使用している高レベルな中間言語です。コンパイル中は当然メモリ上のオブジェクトですが、言語なので文字列表現も可能です。 SILGen によって作られ、 SILOptimizer で加工されて、 IRGen によって消費されます。

SILModule がルートのオブジェクトになります。

主要な要素は関数リストです。SSA(静的単一代入) で構成された CFG(制御フローグラフ) で表現される関数のリスト、と言えばよいでしょうか。SILでは、グローバル関数も インスタンスメソッドも initsubscript も 一つの名前空間 (SILModule) にフラットな関数群として展開され、ASTの様な木構造ではなくなっています。

他にも、VTable、 グローバル変数、 Witness Table、Default Witness Table などの情報を持ちます。詳しくは docs/SIL.rst を参考にしてください。

フロントエンドアクション一覧

swift -frontend <action> で指定するフロントエンドのアクションの一覧です。フロントエンドモードでは必ず何らかのアクションを指定する必要があります。前述したように、フロントエンドモードはパブリックなインターフェイスではなく、追加やインターフェイスの変更が普通に発生するので、このリストは 下記の日付現在のものです。コンパイラのデバッグ用途だけに使われるものもあります。

2017/07/13 現在

コマンドラインオプション
-parse Parse まで実行。メイン出力は無し。終了ステータスで成否判断
-dump-parse Parse まで実行し、その時点での AST をダンプ出力
-typecheck Sema まで実行。メイン出力は無し。終了ステータスで成否判断
-dump-ast Sema まで実行し、 AST をダンプ出力
-print-ast Sema まで実行し、 AST をSwiftソースの形で出力(いろいろ足りてないけど)
-dump-interface-hash Sema まで実行し、APIのインターフェイスのハッシュ値を出力
-dump-scope-maps Sema まで実行し、変数スコープ一覧をダンプ
-dump-type-refinement-contexts Sema まで実行し、 type refinement context ? をダンプ
-emit-tbd Sema まで実行し TBD を出力。どうもモジュールのパブリックなシンボルの一覧ぽい
-emit-imported-modules Sema まで実行し import されるモジュールの一覧を出力
-emit-silgen SILGen まで実行し、raw SIL をテキスト出力
-emit-sibgen SILGen まで実行し、raw SIL をバイナリ出力
-emit-sil SILOptimizer まで実行し、canonical SIL をテキスト出力
-emit-sib SILOptimizer まで実行し、canonical SIL をバイナリ出力
-emit-module6 SILOptimizer まで実行し、 .swiftmodule, .swiftdoc を出力
-immediate IRGen まで実行し、 llvm::ExecutionEngine 上で即実行
-emit-ir LLVM に IR のテキスト出力を依頼
-emit-bc LLVM に Bitcode の出力を依頼
-emit-assembly LLVM に target ネイティブなアセンブリ言語の出力を依頼
-emit-object LLVM にオブジェクトファイル .o の出力を依頼
-emit-pch C,Obj-C の ヘッダファイルを入力として、ブリッジング用のプリコンパイルヘッダーを出力
-repl フロントエンド組み込みの REPL を実行

ソース構成

ライブラリソース
include/swift/Subsystems.h サブシステムのエントリポイントを宣言しているヘッダー
include/swift/ABI ABI関連
include/swift/AST
lib/AST
AST
include/swift/ASTSectionImporter
lib/ASTSectionImporter
swift -modulewrap で実行バイナリに埋め込まれた ASTモジュール を読み込むライブラリ。 lldb から利用
include/swift/Basic
lib/Basic
いろいろ基礎的なライブラリ。基礎だけにいろいろなところで使われてて重要
include/swift/ClangImporter
lib/ClangImporter
ClangImporter
include/swift/Demangling
lib/Demangling
Name Mangling された文字列の Demangle
include/swift/Driver
lib/Driver
ドライバーの実装 (tools/driver から利用される)
include/swift/Frontend
lib/Frontend
フロントエンド
include/swift/FrontendTool
lib/FrontendTool
swift -frontend の実装
include/swift/IDE
lib/IDE
コード補完やシンタックスハイライトなど。主に SourceKit が利用
include/swift/IRGen
lib/IRGen
IRGen
include/swift/Immediate
lib/Immediate
即実行モードの実装
include/swift/Index
lib/Index
インデックサー。 SourceKit が利用
include/swift/LLVMPasses
lib/LLVMPasses
Swift 独自の LLVM Pass
include/swift/Markup
lib/Markup
Markdown 関連 DocComment の処理など
include/swift/Migrator
lib/Migrator
Swift3 → Swift4 マイグレーター実装
include/swift/Option
lib/Option
コマンドラインオプション定義
include/swift/Parse
lib/Parse
Parse
lib/ParseSIL SILパーサー
include/swift/PrintAsObjC
lib/PrintAsObjC
PrintAsObjC
include/swift/Reflection
stdlib/public/Reflection
include/swift/Remote
include/swift/RemoteAST
lib/RemoteAST
実行中のプログラムから ASTを復元する。lldbが使用
include/swift/SIL
lib/SIL
SIL
lib/SILGen SILGen
include/swift/SILOptimizer
lib/SILOptimizer
SILOptimizer
include/swift/Sema
lib/Sema
Sema
include/swift/Serialization
lib/Serialization
ModulerLoader, SILModuleLoader,
include/swift/SwiftDemangle
lib/SwiftDemangle
外部プログラム用 Demangle インターフェイス
include/swift/SwiftRemoteMirror
stdlib/public/SwiftRemoteMirror
Reflection の C ラッパー
include/swift/Syntax
lib/Syntax
空白なども保持する構文木モデル。 Refactoringツールなどが使用してるっぽい
include/swift/TBDGen
lib/TBDGen
-emit-tbd 実装

各ライブラリの依存関係を説明しようと思ったのですが、循環依存ありありで大変な状況なので(略

おわりに

Swift コンパイラの各サブシステム概要でした。各々の詳しい処理については触れませんでしたが、すべてのサブシステムの内容を完全に理解している必要はありません。むしろ全てを把握している人なんていない、はず。興味ある分野のコードからぜひ覗いてみてください。

レッツ エンジョイ コードリーディング!


  1. 最近まで run も対象だったのですが、組み込みの swift runSE-0179 の swiftpm の機能と被るので廃止になりました。 SR-5332 

  2. Swift3 まで Linux では組み込み REPL はサポートされていなかったので、 lldb が無い場合エラーになっていたのですが、実装されたようです。PR-7709 

  3. macOS版では、Mach-O のリンクでは必要ないのでインストールされません(シンボリックリンクが張られません。) 

  4. swiftc -g でコンパイルしたときに使用され、バイナリに埋め込まれてデバッガが使用します。 

  5. 型情報に関知しないといっても、タイプ名のパースはもちろん行います。ただ、それらパースしたタイプ名が実際には何者であるかということは Parse の時点ではわかりません。 

  6. -emit-module は他のアクションとも組み合わせられるのですが、他のアクションが指定されていない場合にはプライマリのアクションとして実行されます。