エラーメッセージ定義を掘り下げる

Swift のコンパイルエラーの仕組み

samekard
Swift・iOSコラム
10 min readApr 29, 2018

--

ここでは Swift のコンパイルエラーのメッセージ定義を改めて掘り下げる。

メッセージ定義は .def ファイルの中でこのような形で書かれている。

ERROR(extra_rbrace,none,
"extraneous '}' at top level", ())

ERROR の他に WARNINGNOTE もあるが形は同じである。

これがさらに利用方法に応じて様々な形に変えられる。その変換処理は #define 機能を使って非常に複雑に(どこが走っているかわかりにくい状態で)行われる。

各 .def ファイルに対応する.hファイルによる利用

.def ファイルには、それぞれ対応する .h ファイルがあり、まずはそのペアのつながりで .def ファイルがどう利用されるかを見ていく。

.def ファイルと .h ファイルのペアというのは、例えば

DiagnosticsParse.def

に対して

DiagnosticsParse.h

があるということ。

.def ファイルの中身は

#ifndef ERROR
# define ERROR(ID,Options,Text,Signature) \
DIAG(ERROR,ID,Options,Text,Signature)
#endif
それぞれのメッセージ定義が大量#ifndef DIAG_NO_UNDEF
# if defined(DIAG)
# undef DIAG
# endif
# undef ERROR
#endif

となっている。ポイントを挙げると

  • ファイルの頭で、 ERRORがすでに #defineされているかどうかを確認し、されていない場合には #defineを行い、ERRORDIAG に置き換える設定をする(引数の表記は省略)。
  • 個々のメッセージ定義の後、最後に DIAG_NO_UNDEFの状態によりdefine 状態の消去を行う。

ちなみに、 WARNINGNOTE は構造が ERROR と同じなので省いてある、ここから先も同じ。

対応する .h ファイルの中身は

#include "swift/AST/DiagnosticsCommon.h"#define DIAG(KIND,ID,Options,Text,Signature) \
extern detail::DiagWithArguments<void Signature>::type ID;
#include "DiagnosticsParse.def"
  • DIAG#defineされて、
    extern detail::DiagWithArguments<void Signature>::type ID;
    になる。
  • その後で .def ファイルが #include される。

したがって、この .h ファイルを読み込んだ場合は、

ERROR -> DIAG -> detail::DiagWith~

という変換がされる。その過程は、

  • .h ファイル内で ERROR#defineされていないので、.def ファイル内の ERROR から DIAG への #defineが有効になり、
  • その DIAG が .h ファイル先頭の #defineにより detail::DiagWith~に置き換えられる。

となる。

また、 DIAG_NO_UNDEF#defineされていないので、.def ファイルの最後で define 状態が消される。

Common.h と Common.def

.h をよく見ると DiagnosticsCommon.h を #include している。この DiagnosticsCommon.h (とそのペアの DiagnosticsCommon.def) は、コンパイルの中で汎用的に利用されるもの。やっていることは上と同じで

ERROR -> DIAG -> detail::DiagWith~

の変換を行い、最後に define 状態を消す。

DiagnosticsAll.def でも読み込んでいる

上に書いたのは各 .def ファイルに対応した .h ファイルを呼んだ時のことであるが、他にも各 .def ファイルを呼んでいるところがある。 DiagnosticsAll.def である。これを見ていく。

#include の矢印はいったいどっち向きが正しいのか
#ifndef ERROR
# define ERROR(ID,Options,Text,Signature) \
DIAG(ERROR,ID,Options,Text,Signature)
#endif
#define DIAG_NO_UNDEF#include "DiagnosticsCommon.def"
#include "DiagnosticsParse.def"
#include "DiagnosticsSema.def"
#include "DiagnosticsClangImporter.def"
#include "DiagnosticsSIL.def"
#include "DiagnosticsIRGen.def"
#include "DiagnosticsFrontend.def"
#include "DiagnosticsDriver.def"
#include "DiagnosticsRefactoring.def"
#undef DIAG_NO_UNDEF#if defined(DIAG)
# undef DIAG
#endif
#undef ERROR

このDiagnosticsAll.defは全ての .def ファイルを読み込むものである。真ん中の #include の連続で読み込んでいる。

  • まず先頭でERROR#define されているか確認し、されていなければ ERRORから DIAG への変換を設定している。
  • また、各 .def ファイルを読み込む前に DIAG_NO_UNDEF#defineしている。これは各 .def ファイル内で ERROR#defineを消さないようにするためである。
  • 各 .def ファイル内では、すでに ERROR#defineされている場合は 再#defineしないので、このDiagnosticsAll.defの先頭の ERROR#defineが全体に渡り有効である。
  • DiagnosticsAll.defファイルの最後では例によって #defineしたものを削除している。

DiagnosticsAll.def を読むところ

ではこのDiagnosticsAll.def を読むところはどこか。

  • /lib/AST/DiagnosticEngine.cpp
  • /lib/AST/DiagnosticList.cpp

である。

/lib/AST/DiagnosticEngine.cpp について

DiagnosticsAll.def#includeしているところを見ていく。

enum LocalDiagID : uint32_t

enum LocalDiagID : uint32_t {
#define DIAG(KIND, ID, Options, Text, Signature) ID,
#include "swift/AST/DiagnosticsAll.def"
NumDiags
};

LocalDiagIDという enum を宣言している。ここでは DIAG から IDを取り出す #define をしてから DiagnosticsAll.defを読みこんでいて

ERROR -> DIAG -> IDのみ

という変換が行われる。

置き換え先( ID,)の最後の , は emun の要素の区切りになる。エラー識別文字列による enum を作って、ひとつひとつに数値を与えていることがわかる。0から順番に番号が振られて、最後の NumDiags に個数が入る。

static StoredDiagnosticInfo storedDiagnosticInfos[]

static StoredDiagnosticInfo storedDiagnosticInfos[] = {
#define ERROR(ID, Options, Text, Signature) \
StoredDiagnosticInfo(DiagnosticKind::Error, DiagnosticOptions::Options),
#include "swift/AST/DiagnosticsAll.def"
};

StoredDiagnosticInfo の配列を宣言している。StoredDiagnosticInfo については未開拓。

ここでは ERROR#define しているので

ERROR -> StoredDiagnosticInfo

という変換が行われる。ここで ERROR#define されると、この先で ERROR の上書き #define はされない。DIAG を経由しないで ERROR を直接料理している。

static const char *diagnosticStrings[]

static const char *diagnosticStrings[] = {
#define ERROR(ID, Options, Text, Signature) Text,
#include "swift/AST/DiagnosticsAll.def"
"<not a diagnostic>",
};

ここは Text 部分を取り出して配列を作るところ。 上と同じように ERRORの直接料理で、

ERROR -> Text のみ

という変換がされる。

/lib/AST/DiagnosticList.cpp について

次にもう一つのほう、DiagnosticList.cpp を見ていく。

enum class swift::DiagID : uint32_t

enum class swift::DiagID : uint32_t {
#define DIAG(KIND,ID,Options,Text,Signature) ID,
#include "swift/AST/DiagnosticsAll.def"
};

ここは ID 部分を取り出して enum を作るところ。ここもエラー識別文字列による enum を作って、ひとつひとつに数値を与えていることがわかる。

detail::DiagWithArguments<void Signature>::type ID

#define DIAG(KIND,ID,Options,Text,Signature) \
detail::DiagWithArguments<void Signature>::type ID = { DiagID::ID };
#include "swift/AST/DiagnosticsAll.def"

ここも置き換え。

extern detail::DiagWithArguments<void Signature>::type ID = { DiagID::ID };

に変換される。

この type は /include/swift/AST/DiagnosticsCommon.h で

template<typename T>
struct DiagWithArguments;
template<typename ...ArgTypes>
struct DiagWithArguments<void(ArgTypes...)> {
typedef Diag<ArgTypes...> type;
};

と定義されているが深そうなので、一旦切り上げる。

以上、メッセージ定義がどういう形になっていくかを見てきた。当然ここから先も調べる項目は残されている。調べたらまた記事にしたいと思う。

内容に間違いがあった場合、お知らせ下さい。

--

--

samekard
Swift・iOSコラム

iOSアプリをいろいろ作りました。英語と中国語を勉強中。