C言語のプリプロセッサを自作して簡単なマクロは扱えていたが、 いろいろなソースを食わせてみたところうまく展開できないケースがあったので修正した。
前置き
マクロ定義で自分を含む再帰的な定義をしてしまうと、展開が無限に続いてしまう可能性がある:
マクロ展開は単純な文字列置換で行われるので、終了条件で回避することができない。 なのでそういうことが起こらないように、一段階しか展開されないようになっている:
RECUR(123) // => RECUR(123-1) |
という程度の理解だったので、「ふむふむ展開中はそのマクロ名は無効にしてやればよいのね」という具合で実装していた。
うまく展開できなかったケース
なんかのソースでうまく展開できないのを掘っていったところ、次のようなケースに問題があった:
|
関数風マクロで実装されたAPIがチェックをかましつつ他の関数風マクロAPIを呼び出してて、そちらでもチェックが入る、というような構成。
マクロ展開を関数呼び出しと同じように考えると、
F
の展開でCHECK
の展開が呼び出され、さらにG
の展開が呼び出されCHECK(H(x))
となるが、CHECK
は展開中だからここでストップ、
で結果はCHECK(H(987))
となっていた。
が実際にはどちらも展開されてH(987)
となるのが期待される動作のようだった。
アルゴリズム
うまく展開するアルゴリズムは自分で考えてもわからなかったのでググったところ、 C++でCプリプロセッサを作ったり速くしたりしたお話 のスライドに示されていたサイト、 blog dds: 2006-06-26 — Dave Prosser’s C Preprocessing Algorithm から辿れるpdfに書かれていた。
主に expand
と subst
の2関数:
expand(TS) /* recur, substitute, pushback, rescan */ |
subst(IS,FP,AP,HS,OS) /* substitute args, handle stringize and paste */ |
- TSがトークン列、Tがトークンを表し、マクロ展開はトークン単位で行われる
- HSがhidesetを表し、展開しない語彙セットをトークンごとに持つ
subst
はマクロ引数の置換を行うIS
: 入力列FP
: マクロのパラメータ (Formal Parameter)AP
: マクロに与えられた実引数 (Actual Parameter)HS
: HideSetOS
: 出力列
hsadd
で出力列の各トークンにhidesetを追加する
再帰による実装なので追いづらいんだけど、expand
でトークン列を順に見ていき、
マッチしたマクロ名に対しての subst
呼び出しで
IS
をマクロボディ、 OS
を空で与えるので、
マクロで一段展開されたトークン列にそのマクロ名自体がhidesetとして追加されるという動作っぽい。
subst
内での引数置き換え時に、hidesetを指定しない状態で引数だけを先に expand
で展開するので、
引数自体はそのマクロを含んでいても展開される、ということっぽい。
引数付きマクロの場合に、閉じ括弧のhidesetと積をとっているのはなんだろう…よく理解してない、 これがないと無限ループが発生してしまうのかもしれない。
資料では他に ##
の連結や #
の文字列化などについても書かれている。
実装、ループ化
上記のアルゴリズムは再帰呼び出しで副作用なしなので簡潔ではあるけど、そのまま実装すると再起呼び出しとシーケンス連結のコストがかかってしまう。 ループ・副作用あり(配列要素書き換え)にできるにこしたことはない、と思っていじくったところ問題なく動くっぽい。
- hidesetはすべてのトークンには必要なく、マクロ名である可能性がある識別子と、閉じ括弧のみでよい
- マクロボディや引数のトークンは、展開や置換で複数回使用されることになるので、それぞれでhidesetが別になるよう複製する必要がある?
JavaScriptで実装してみた: https://github.com/tyfkda/xcc/blob/main/tool/pp.js