C言語の文法は型の記述が複雑で、特に関数ポインタが絡むと非情にややこしくなる。
例えば signal
という標準関数は
なにかシグナルが発生したときに呼び出される関数ポインタ(シグナルハンドラ)を設定する関数で、
void (*signal(int sig, void (*func)(int)))(int); |
となっている。
パッと見たところどういう宣言なのかわからないけど、これはシグナル番号int sig
と関数ポインタvoid (*func)(int)
を取る2引数関数で、戻り値に以前に設定されていた関数ポインタvoid (*)(int)
が返る。
シグナルハンドラはint
を1つ取る関数で、発生したシグナル番号が渡される。
この文法をパースして対応する型を構築するのに手間取った。 特に再帰下降法とは相性が悪いように思う。
型のBNF
C言語の型に関するBNFを The syntax of C in Backus-Naur form から抜粋すると、
<declaration> ::= {<declaration-specifier>}+ {<init-declarator>}* ; |
(<type-specifier>
はint
やstruct
などの具体的な型、<type-qualifier>
はconst
など)。
<declaration>
ルールの {<declaration-specifier>}+
がベースとなる型の部分で、関数の場合は戻り値の型になる。
<direct-declarator>
ルールの(a)だと単純にその型、
(b)の ( <declarator> )
は <declarator>
のポインタが結合する先の変更、
(c)のブラケットが配列型、(d)が関数型となる
((e)はなにかわからんのでひとまず無視)。
<direct-declarator>
は左再帰になっていて、(a)か(b)の後に、(c)か(d)を任意回受け付ける。
ちなみにこのBNFだと、 int (x)
、はたまた int (((y)))
などという宣言も合法。
再帰下降法によるパースの順序と型の生成のミスマッチ
再帰下降法によって上記の型のパースを擬似コードとして書き下すと(<init-declarator>
を省略して)
declaration() { |
direct_declarator
では左再帰を再帰下降法に変換するために、後半部分を別関数にして再帰させている。
実際には各関数からの戻り値としてパースによって構築された型を返す。
問題はdirect_declarator
内の(1)と(2)の部分で、パースの順序としては(1)→(2)の順なんだけど、型の入れ子順としては(2)→(1)とする必要がある。
具体的には例えば signal
の場合、宣言の最初の括弧 (*signal...)
のアスタリスクによるポインタ指定は戻り値型にかかるが
そのパース時点では型がvoid
までしか読まれておらず、括弧外最後の(int)
は読まれてないので型が決定してない。
なのでその時点でどの型に対するポインタ型かというのを構築することができない。
対処法
プレースホルダーとして型を作成して(1)のdeclarator()
に渡すようにして、戻り値が結果の型になる。
(2)のdirect_declarator_suffix()
にはdirect_declarator()
に渡された型を渡す。
その結果がプレースホルダーの中身となるので、memcpy
してやる。
実際のソースの該当箇所はこちら。
他のソースを見てみる
chibicc
低レイヤを知りたい人のためのCコンパイラ作成入門の参考リポジトリとなるchibiccを見てみた。 箇所はdeclarator関数で、
static Type *declarator(Token **rest, Token *tok, Type *ty) { |
「memcpy
してない…!」と驚愕したんだけど、トリックはdeclarator()
呼び出し、type_suffix()
呼び出しに続いて(3)で再度declartor()
を呼び出しているところ。
chibiccでは最初にソースをすべてトークンに分割してリンクリストにしていて任意の箇所からパースできるのがポイントで、
最初のdeclarator()
呼び出しはトークンを読み進めるだけで結果はdummy
に受け取るが使用しておらず、
型の構築としてはtype_suffix()
が先に行われて、もう一度同じトークンstart->next
からdeclarator()
呼び出しの結果を最終的な型としている。
続きのトークンはtype_suffix()
で消費した次のトークンがrest
に格納され、辻褄が合う。
8cc
chibiccの前身であるところの8ccも見てみた。 箇所はread_declarator関数で、
static Type *read_declarator(char **rname, Type *basety, Vector *params, int ctx) { |
で、こちらはトークンを逐次得る方式のようで、stub
を渡しておいて後から中身にはread_declarator_tail()
の結果を書き込むようになっている。
感想
- この文法になったのもクレイジーだし、それをパースできるようにしたのもよくできたなぁという感想。
- 単に関数ポインタのアスタリスクの位置がまずかっただけの問題な気もするが…普通の後置アスタリスク
(void func(int))*
だったら複雑にならなかったかも。 - yaccとか使ったらまだマシなのかしら?と思って試してみたが(RaccによるC言語パーサを使って)
direct_declarator
のアクション型生成すると型のネスト方向が逆になってしまい、構文木を構築して後から型を構築するのもいろいろ大変そうな気がした。 - 「低レイヤ」のネストしている型の読み方に、
signal
関数の型の読み方が書いてあってオススメ。- またそれを含むCの型の構文で型の内部表現がわかりやすい。