Swift のコンパイルエラーの仕組み
コンパイルエラーが出たときに原因を(コンパイラのソース分析的に)探りたいときがある。その水深 1cm までをまとめてみたい。
これを読む前提(水深0.1cm)
手元に Swift のコードがあること。下の場所から入手できる。私は Download Zip ボタンでダウンロードした。
エラーメッセージのありか(水深0.3cm)
普段、プログラミング中に目にするエラーメッセージの文章は
/include/swift/AST
の中にある
Diagnostics***.def
という、拡張子が def のファイルにまとめて書かれている。このファイルは複数あって、***には細分化された名前が入る。
以下のものがある。
- DiagnosticsParse.def
- DiagnosticsSema.def
- DiagnosticsClangImporter.def
- DiagnosticsSIL.def
- DiagnosticsIRGen.def
- DiagnosticsFrontend.def
- DiagnosticsDriver.def
- DiagnosticsRefactoring.def
- DiagnosticsCommon.def
- DiagnosticsAll.def
その中で、DiagnosticsParse.def の一部を抜粋するとこんな感じである。これは閉じ波括弧が多いというエラーメッセージ。
ERROR(extra_rbrace,none,
"extraneous '}' at top level", ())
定義数が多いものは、Swift4.1 の時点で
- DiagnosticsParse.def に500以上
- DiagnosticsSema.def に800以上
ある。
この記事ではこの Diagnostics***.def を「メッセージ定義ファイル」または「.def ファイル」ということにする。
メッセージ定義の形式(水深0.7cm)
メッセージ定義ファイル内の個々の定義は基本的に
ERROR(ID,Options,Text,Signature)
の形で行う。
- ID
エラーを識別する文字列。エラー発生処理でエラーを指定するのに用いる。エラーメッセージの内容に沿ったものが多い。単語の区切りを表現するのはスペースはなく_で行う。 - Option
何か付加情報を入れるようだが何もないことが多い - Text
実際に我々が目にするエラーメッセージの原型。コンパイル時に確定する部分は%が付けられている。これはC言語の printf や Obj-C の NSLog 方式に似ている。 - Signature
エラーメッセージの中でコンパイル時に確定する部分の型をタプル形式で与える。Text の中でこれらが入る部分にはあらかじめ%が付けられている。
さっきのメッセージをあらためて見ると
ERROR(extra_rbrace,none,
"extraneous '}' at top level", ())
ID が extra_rbrace
Option がnone
Text が "extraneous '}' at top level”
Signature は ()
でコンパイル時に確定するものは何もない。
メッセージ定義の注意点は3つある。
ひとつは、 Text はコンパイル時に決定する情報が抜けているので、表示されたメッセージの文字列を検索する際は、そこをうまく予測して省いてやること。
もうひとつは、Textの項目は頭文字が小文字で書かれていても、Xcode 上でメッセージの頭文字は大文字で書かれるということ。検索の際に注意が必要。定義されたメッセージの頭が super.init() のときに、Xcode 上の表示はSuper.init()となるのはこれが原因である。
最後に、長いメッセージは分割して定義されているということ。例えば enum の Raw 値の型がふさわしくない時のメッセージ本体は
"%0 declares raw type %1, but does not conform to RawRepresentable "
"and conformance could not be synthesized"
というように文が分割されている(見やすさのため?)。表示されたメッセージの中からRawRepresentable and conformance
の部分を選択して検索しても引っかからないので注意。
Swift コード解析処理からエラーを出す処理(水深0.9cm)
Swift コード解析処理において、エラーが発覚したところで
diagnose(P->getLoc(), diag::cannot_infer_type_for_pattern);
というように diagnose()
関数を呼んでエラーメッセージ発生処理を行う。この例では1番目の引数にソースコード上の場所、2番目の引数に、エラー識別の文字列(エラー定義部のID)を与える。
必要な情報がコンパイル時に決定する場合はそれも引数に追加するので、この関数はいくつかパターンがある。
まとめ(水深1.0cm)
ここまでの知識により、コンパイルエラーを出している場所にたどり着くことが出来る。つまり、メッセージの文字をたよりにしてメッセージ定義を見つけ、その ID を使って Swift コード解析処理の中を検索する、というやり方で行える。そのあとは個々の C++ のソースコードを頑張って解読することになる。