テキストマクロプロセッサ「M4」のチューリング完全性について (KLabTechBook Vol.1)
この記事は技術書典3にて頒布した「KLabTechBook Vol.1」に掲載したものです。
現在開催中の技術書典14にて新刊「KLabTechBook Vol.11」を頒布(電子版無料、紙+電子 500円)しています。 また、KLabのブログからすべての既刊のPDFを無料DLできます。 合わせてごらんください。
M4とは、1977年にブライアン・カーニハンとデニス・リッチー(いわずと知れたK&Rその人らである)によって作成されたテキストマクロプロセッサである1。 POSIXにも含まれる基本的なコマンドだが2、読者諸氏の多くはこのコマンドを明示的に使ったことは無いだろう。 しかしながら、間接的にM4の恩恵を受けている人は少なくないはずだ。
configure
スクリプト。数多くのOSSで採用されているこのビルド設定スクリプトは、
GNU Autoconfにより呼び出されたM4によって生成されている3。
他にもSendmailの設定ファイルの生成に利用されるなど4、縁の下の力持ちといったツールである。
登場から40年となるこのマクロプロセッサは、驚くべきことにチューリング完全性を備えている。 つまり、コンピュータ黎明期に作られたプログラミング言語のひとつと言っても過言ではない。 しかし、テキストマクロプロセッサ、すなわちテキストの置換という単純作業の繰り返しが チューリング完全になるとは俄に信じがたい。
この章ではそれを確かめるべく、M4のチューリング完全性の証明を試みる。
チューリング完全性の証明方法
チューリング完全性の一般的な証明方法として、 すでにチューリング完全であることがわかっている言語の処理系を実装するという手法がある。 言い換えると、チューリング完全な言語で書かれたプログラムをすべて実行できるならば、 それはチューリング完全である。
ここでは、チューリング完全な言語であるBrainfuck5(BF)をM4で実装することにより、 M4のチューリング完全性を証明する。
実装方法
この節では、M4によるBFの具体的な実装方法について解説する。 なお、ここで扱うM4はGNU M46とし、 実装したM4の完全なコードはGitHubにて公開しているので7、適宜参照されたい。
BFコードの読み込みとマクロ列への変換
まずBFのコードは標準入力から受け取るものとし、
include
マクロを利用して/dev/stdin
をまるごと取り込むようにした。
M4ではBFの命令に使われているような記号そのものをマクロとして処理できないため、 取り込んだBFコードを表1にしたがって、BF命令1文字ずつ対応する文字列に変換し、マクロの列に変換した。 この処理を行うM4マクロをリスト1に示す。
表1 BF命令と変換文字列の対応表
BF命令 | 変換文字列 | |
---|---|---|
ポインタのインクリメント | > |
`'ip |
ポインタのデクリメント | < |
`'dp |
ポインタの指す値のインクリメント | + |
`'ic |
ポインタの指す値のデクリメント | - |
`'dc |
ポインタの指す値の表示 | . |
`'pr |
ループ開始点 | [ |
`'bn(` |
ループ終点 | ] |
')`'ed |
リスト1 BFコードを読み込みマクロ列へ変換する
# 引数の1文字を対応する文字列に変換するマクロ
define(`token', `changequote({,})ifelse(
{$1}, {>}, {`'ip},
{$1}, {<}, {`'dp},
{$1}, {+}, {`'ic},
{$1}, {-}, {`'dc},
{$1}, {.}, {`'pr},
{$1}, {[}, {`'bn(`},
{$1}, {]}, {')`'ed}){}changequote')
# 1文字ずつtokenマクロにかけるマクロ
# 引数の長さが0になるまで再帰的に呼び出す
define(`parse', `ifelse(
eval(len(`$*')>0), 1,
`token(substr(`$*',0,1))`'parse(substr(`$*',1))')')
# 標準入力をまるごとparseマクロにかけ、その結果をinternalとして定義する
define(`internal', parse(include(`/dev/stdin')))
ここで定義したparse
マクロは、引数文字列の先頭1文字を取り出してtoken
マクロに渡し、
残りの文字列を自分自身に再び渡すという、再帰的なマクロである。
このマクロにより、たとえば!
1文字を表示するBFのプログラムを変換すると
リスト2のようになる。
リスト2 !
を表示するBFプログラムと変換後のマクロ列
変換前:
>++++[-<++++++++>]<+.
変換後(改行は含まない):
`'ip`'ic`'ic`'ic`'ic`'bn(``'dc`'dp`'ic`'ic`'ic`'ic`'ic`'ic`'ic`'ic`'ip')
`'ed`'dp`'ic`'pr
ここで`'
は空文字列で、マクロ同士のデリミタとして働く。
また、BFにおいてループを表す[〜]
は、bn(`〜')`'ed
のように展開され、
間に入るマクロ列がまるごとクォートされた文字列としてbn
マクロの引数になるように変換される。
ポインタとバイト配列
BFのメモリ空間は、30000要素以上のバイト配列とその上を自由に動くポインタで構成される。
M4には配列が無いため、バイト配列をそのまま実装することはできない。
そこで、数字入りのマクロを動的に定義することで擬似的な配列とした。
つまり、_0_
、_1_
、…_30000_
といった具合のマクロを配列の要素として扱う。
ポインタはptr
というマクロを初期値0
として定義し、この定義を動的に書き換えることで実現した。
ポインタの値はdefn
マクロでptr
の定義を取り出せば参照でき、
ポインタの指す値は_
でポインタの値を挟んだマクロ名の定義を取り出せばよい。
これらの処理の実装をリスト3に示す。
リスト3 ポインタとバイト配列の実装
# ポインタの定義. 0で初期化
define(`ptr', `0')
# 擬似的な配列からポインタの指す値を取り出すマクロ
define(`cv', `eval(defn(`_'defn(`ptr')`_')+0)')
cv
マクロはeval(〜+0)
と実装した。
これにより、たとえば_1_
が未定義だった場合その定義は空文字列になるので、
cv
はeval(+0)
となり、0
に展開される。
バイト配列の要素の初期値は0クリアされているというBFの仕様をこのような形で実現した。
各命令に対応するマクロの実装
ここからBFの各命令に対応するマクロを実装していく。
ポインタのインクリメント・デクリメント(>
<
)
前述のとおり、ポインタの操作はptr
マクロを動的に書き換えることで実現した。
リスト4 ポインタのインクリメント・デクリメントの実装
# ポインタのインクリメント (>)
define(`ip', `define(`ptr', incr(defn(`ptr')))`'')
# ポインタのデクリメント ptr (<)
define(`dp', `define(`ptr', decr(defn(`ptr')))`'')
M4ではdefine
マクロで既存の定義を上書きできることに加え、
incr
・decr
という引数の値をインクリメント・デクリメントするマクロも用意されている。
これを利用して、「defn(`ptr')
で取り出したポインタの値をincr
マクロに渡し、
その結果を新たなptr
として定義しなおす」という文字列に展開されるマクロとしてip
を定義した。
つまり、処理対象のテキストにip
が現れるたび、
define(`ptr', incr(defn(`ptr')))`'
という文字列に置き換わり、それが処理される。
ポインタのデクリメントはincr
の代わりにdecr
を使う以外は同じである。
ポインタの指す値のインクリメント・デクリメント(+
-
)
この命令もM4のincr
・decr
を利用し、
すでに定義したcv
マクロで取り出した値を処理するようにした。
書き換えるべきマクロ名は、前述のとおり、_
でポインタの値を挟んだ文字列である。
リスト5 ポインタの指す値のインクリメント・デクリメントの実装
# ポインタの指す値のインクリメント (+)
define(`ic', `define(`_'defn(`ptr')`_', incr(cv))`'')
# ポインタの指す値のデクリメント (-)
define(`dc', `define(`_'defn(`ptr')`_', decr(cv))`'')
ループ([
]
)
リスト6 ループの実装
# ループ開始点 ([)
define(`bn', `pushdef(`s', `ifelse(eval(cv>0), 1, `$1`'s')')')
# ループの終点 (])
define(`ed', `s`'popdef(`s')')
BFで[ ]
に囲まれた部分に対応するマクロ列が、
クォートされた文字列としてbn
マクロの引数になることはすでに解説した。
bn
ではs
という名前の、
「cv
が0より大きい間、引数($1
)をそのまま展開してからs
自身を呼び出す」という
再帰的なマクロを定義している。
そしてループ終点にあたるed
マクロにより、
bn
で定義されたs
が展開され、ループが始まる。
ここでs
の定義にはdefine
ではなくpushdef
を使っている。
M4ではpushdef
・popdef
によりマクロの定義をスタックできる。
これを利用し、多重ループでもs
の定義を内側のループで上書きしてしまわないよう実装した。
ポインタの指す値の表示(.
)
M4にはC言語のprintf
同様のフォーマットで書式化できるformat
マクロがある。
ポインタの指す値をそのまま%c
で書式化する方法で実装した。
リスト7 ポインタの指す値を表示するマクロの実装
define(`pr', `format(`%c', cv)')
プログラムの実行
ここまででBFの各命令に対応するマクロが定義できた。
この後にBFプログラムから変換しておいたマクロ列、すなわちinternal
マクロを展開することで、
マクロ列の中の命令マクロが順次展開され、プログラムが実行される。
チューリング完全性の証明
これにてM4によるBFの実装は完了である。 実際に動作するかどうかはぜひ各々で検証してほしい。
さて、ここでBFに詳しい読者諸氏であれば、命令がひとつ足りないことに気づいたであろう。
そう、標準入力から1文字読み込む,
命令である。
今回の実装では標準入力をBFプログラムの入力としてしまっている都合上、
,
にあたる命令を実装することは不可能である。
しかし、その命令によって入力したい文字がたとえばA
ならば、
,
の代わりに+
を65個並べることで代用できる。
つまり、すべての,
は別のBFコードに置き換えることが原理的に可能なので、
この命令が無かったところでチューリング完全性が失われることはない。
このように、M4により動作するBFのチューリング完全なサブセットを実装することができた。 したがって、テキストマクロプロセッサ「M4」はチューリング完全である。
終わりに
広く使われているテキストマクロプロセッサといえば、C言語のプリプロセッサを思い浮かべる人も多いだろう。 しかしM4と違い、Cプリプロセッサ単体ではチューリング完全ではない。 この差の要因のひとつは、マクロの再帰的な呼び出し(あるいはループ)ができるかどうかである。
テキストマクロプロセッサにチューリング完全性が必要かどうかはさておき、 M4はチューリング完全性を備えた強力なツールであることは理解していただけたと思う。 そしてその歴史も長く広く使われており、これ以上ないほど安定している。
もし定型的なテキストを量産するような作業に出会った時、 貧弱なテンプレートエンジンを自作する前にM4のことを思い出してみてはいかがだろうか。 書式はややまどろっこしいかもしれないが、やりたいことがすべてできることは証明済みだ。
まあそんなこと言っても積極的に使ったりしないですけどね。