Stack Machine
についてこれからは、このインタープリターでつくったtiny Cについて、コンパイラを作っていくことにする。最終的には、マシンコードを直接出力するコンパイラを作るが、コード生成の考え方を簡単にするために、初回に紹介したスタックマシンをターゲットにする。スタックマシンではレジスタを扱わなくても良いため簡単になる。初回では単純な数式のコンパイルを考えたが、言語を実行するためにはインタプリターでやったように関数呼び出しやローカル変数をどのように作るかを考えなくてはならない。コンパイラのターゲットの仮想マシンの解説からはじめることにしよう。
仮想スタックマシンの命令
tiny Cのターゲットとして考えるマシンの命令は、以下の20個の命令である。
なお、この命令を持つ仮想マシンのインタプリタを作ってみた。st_machine.cを参照のこと。POPや、PUSHI, 演算ADD,SUBなどは、1回目の講義で解説した通り、スタックに値をセットしたり、演算したりする命令である。コンパイルでは、このスタックマシンのコードを使って、式を実行するコード列を作る。その手順は、
さて、変数はパラメータや局所変数があるが、どこからとってくればいいのであろうか。(ここでは、大域変数は考えないことにする。)
関数呼び出しの構造
式だけを評価するならば、これでいいが、関数が使えるようにするためには、スタックの使い方を工夫しなくてはならない。スタックマシンは以下の3つのレジスタを持つ。
関数の呼び出しの手順は、以下のようにする。
このような構造を、関数フレームと呼ぶ。このような規則を関数のリンク規則(linkage convensionあるいはcalling sequence)とよび、各マシンごとに定められている。
さて、関数定義に対するコードは以下のようになる。
ENTRY foo
FRAME ローカル変数の個数
.... 関数本体のコード....
RET
また、関数呼び出しは、
引数1の
push ...引数2のpush ...
....
CALL foo
POPR push
した引数の個数関数のコンパイル
(CompileFuncDef)は、以下のようになる。パラメータの変数や局所変数は、スタック上にその領域が確保されるが、どこに確保されるかを数えておかなくてはならない。この変数がどこに割り当てられているかを覚えておくために、インタプリータで使った環境
Envと同じようなデータ構造をつかう。コンパイラでは、Envでコンパイルしているときにどの変数がスタック上のどこに割り当てられているかを覚えておく。パラメータについては、パラメータの何番目かについて、Envに登録しておく。関数の本体に
block文(Cの{}にあたる)がある場合には、局所変数が定義される可能性がある。block文をコンパイルするときには、local_var_posという変数を使って数えて、これでスタック上の何番目に割り当てるかを決める。本体のコンパイルが終わると、局所変数が何個合ったかがわかるので、これを使って関数の先頭で、FRAME命令を生成しなくてはならない。そのため、生成されたコードを配列(Codes)にとっておき、ENTRY命令の後にFRAME命令を生成し、とっておいた残りの命令を出力する。関数呼び出しのコンパイル(
compileFuncCall)は、引数をスタックに積んで、CALL命令を出す。引数をスタックに積むのは、式の実行が終わるとスタック上につまれるはずなので、単に引数をコンパイルすればよい。その後に、CALL命令を生成し、その後で、引数をスタックからpopして、結果をpushする命令POPR命令を生成しておく。スタックに積む順番は、引数の最後からなので、引数の最後からコンパイルしなくてはならないことを注意しよう(CompileArgs, この関数は同時に引数の個数を数えている)。局所変数のコンパイル
block文では、局所変数が宣言されることがあるが、以下のようにしてコンパイルする(comileBlock)。
なお、代入文(式)も、値を持つ。したがって、
STOREA/STORELはスタックのtopの値を変数(の位置)に格納するだけで、値はそのままスタックに残しておいていることに注意。制御文のコンパイル
JUMP
命令は、LABEL文で示されたところに制御を移す命令である。このスタックマシンは分岐命令は、BEQ0命令しかない。この命令は、スタック上の値をpopして、これが0だったら、分岐する命令である。これを組みああわせてIF文をコンパイルする。コードは次のようになる。...条件文のコード...
BEQ0 L0 /* もし、条件文が実行されて、結果が0だったら,Lに分岐*/
...then
の部分のコード...JUMP L1
LABEL L0
... else
の部分のコード...LABEL L1
IF
文のコンパイルは以下のようになる。なお、
else部がないときには、IF文の値が0とするために、PUSHI 0をおいておかなくてはならないことを注意。return文のコンパイルは
だけで、よい。
課題で、
WHILE文を考えてみよう。出力文の
printlnは、ちょっと変則なので、特別に扱ってある。コンパイラとスタックマシンの実行
さて、
web上にあるプログラムをコンパイルするとコンパイラtiny_ccとスタックマシンのインタプリターst_machineができる。tiny_ccは、標準入力から呼んで、コンパイルの結果のコードを標準出力に出力するようになっている。例えば、プログラムfoo.cをコンパイルして、コードfoo.iを作るには、%tiny_cc < foo.c > foo.i
とすればよい。
st_machineもコードは標準入力から読むようになっているので、%st_machine < foo.i
とすればよい。もしも、連続して動かす場合には、
%tiny_cc < foo.c | st_machine
としてもよい。
演習課題7:
www上においてあるtiny Cのコンパイラではwhile文をわざと抜いてある。while文をコンパイルする部分を付け加え、以下のプログラム(fac.c)をコンパイルできるようにし、実行せよ。提出は、修正したところのみでよい。
余裕のある人は、
for文のコンパイルを考えてみよ。main()
{
println("fac is %d", fac(10));
}
fac(n){
var i,s;
i = 1;
s = 1;
while(i < n){
s = s * i;
i = i + 1;
}
return s;
}