コンパイラ作成(31) ローカル変数
今回の目標
変数のサポート頑張るよ。
// 変数 int main() { int answer; answer = 42; printf("Answer is %d\n",answer); }
ローカル変数一個の単純な場合から始める。グローバル変数については当面放置かな。
デバッグ出力
本題に行く前に準備工作。
$opt_d = false if ARGV[0] == "-d" then $opt_d = true ARGV.shift end
メイン部に追加したよ。デバッグ用出力をon/offするとき毎回ソース弄ってたんだけど面倒くさいしoffにするの忘れがちなんでオプションにした。もっと前からやっとけば良かったな。式の構文解析するとき頻繁にon/offしてたもんなあ。
変数の宣言
それじゃここから変数のサポートいくよ。
# コンストラクタ def initialize(fname) @fname = fname # ソースファイルのファイル名 @asmfname = fname.sub(/\.myc$/,'.s') # アセンブリコードのファイル名 @exefname = fname.sub(/\.myc$/,'') # 実行ファイル名 # print "asmfname=",@asmfname,"\n" @lex = Lexer.new(@fname) @funcname = nil # 現在処理している関数名 @literalcnt = 0 # 文字列リテラルの数 @literaltable = [] # 文字列リテラルのリスト @needprint = false # print.oのリンクが必要か @functions = Hash.new # 関数 @lvars = nil # ローカル変数 @lvarsize = nil # スタックに確保する領域のサイズ end
Parserのinitializeにローカル変数用のデータを二つ追加。@lvarsにHashで変数名を登録していく。
elsif kind == TK::RESERVE && str == "int" then # 変数宣言の処理 loop do kind, str = @lex.gettoken if kind != TK::ID then perror end print "var "+str+"\n" if $opt_d @lvarsize += 4 if @lvars[str] then perror "redefinition variable \"" + str +"\"" end @lvars[str] = ["int",@lvarsize] kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "," then break end end if kind == TK::SYMBOL && str == ";" then return true end perror "expected ';' after variables" return true;
変数宣言の処理はこんな感じ。statementメソッドに追加。Hashの中身は型情報とrbpからの変位。実際にローカル変数の参照等で使うやつね。
スタックフレーム
ローカル変数はスタック上に領域を確保するよ。
# 関数の構文解析 def function() @lvars = Hash.new @lvarsize = 0; rettype = nil kind, str = @lex.gettoken if kind == TK::RESERVE && str == "int" then rettype = str kind, str = @lex.gettoken end if kind == TK::EOF then return false end if kind != TK::ID then perror "expected identifier" end @funcname = str kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "(" then perror end kind, str = @lex.gettoken if kind != TK::SYMBOL || str != ")" then perror end codegen ".global "+@funcname codegen @funcname+":" codegen " push rbp" codegen " mov rbp, rsp" codegen " sub rsp, 64" kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "{" then perror end while statement do end codegen ".RET_" + @funcname + ":" codegen " add rsp, 64" codegen " pop rbp" codegen " ret" if @functions[@funcname] != nil then perror "redefinition of \"" + @funcname + "\"" end @functions[@funcname] = [rettype,[]] p @lvars if $opt_d # デバッグ用 @funcname = nil return true end
ここでスタックフレームを作成してる。あと、ローカル変数を管理するHashの初期化もここでやってるよ。今回、一番悩んだのがこの処理だよ。問題はスタック上に何バイト確保すれば良いか分からない点。K&Rの時代と違って今の時代のC言語は変数の宣言が自由なんで、関数を最後まで見てかないと確定しないんだよね。どうすれば良いのか少し考えてみたよ。
モダンなコンパイラは大体3番目の方法でやってるのかな。mycは直接x86_64のコードを生成してるでこの方法にするのは大変だな。ってことで今回は悩んだ挙句、固定値で64バイト確保するようにしちゃった。int変数16個分なんで当面はこれで良いかな。将来はなんとかしたいなあ。
return文
スタックフレームの関係で単純にretするわけにいかなくなったんで修正。
if kind == TK::RESERVE && str == "return" then # return文の処理 kind, str = @lex.gettoken kind, str = expr kind, str codegen " jmp .RET_" + @funcname if kind == TK::SYMBOL && str == ";" then return true end perror "expected ';' after return statement" return true;
retの代わりにjmpするようにしてる。
式/ラベルの処理
ここから変数を含んだ式の解析。
elsif kind == TK::ID then # 式/ラベルの処理 idname = str skind, sstr = @lex.gettoken if skind == TK::SYMBOL && sstr == ":" then # ラベルの処理 codegen ".LBB_" + @funcname + "_" + idname + ":" else kind, str =expr2 kind, str, skind, sstr if kind == TK::SYMBOL && str == ";" then return true end perror "expected ';' after expression" end return true;
ラベル定義じゃないTK::IDは変数もしくは関数なんでexprを呼ぶようにした。
expr
次は式の構文解析部。
# 式の構文解析 def expr2(fkind,fstr,skind,sstr) el, kind, str = read_el fkind, fstr, skind, sstr puts to_str(el) if $opt_d # デバッグ用 el = modify_el el, ["*","/"] el = modify_el el, ["+","-"] el = modify_el el, ["=="] puts to_str(el) if $opt_d # デバッグ用 codegen_el el return kind, str end
代入演算子に対応するために一行追加した。これで単純な場合は上手くいくんだけど、a=b=42;みたいな場合は上手くいかないよ。代入演算子は他の演算子と違って右結合なんだけどその処理が入ってないからね。modufy_elで左結合、右結合をちゃんとやらないといけない。
コード生成
お次は代入演算子のコード生成。
# 式のコード生成 def codegen_el(el) if (el.size > 1) && el[1].str == "=" then codegen_assign el else codegen_elf el.shift loop do if el == [] then break end op = el.shift operand = el.shift codegen_els op, operand end end end # 代入のコード生成 def codegen_assign(el) if el.size != 3 then perror end if !el[2].kind_of?(Array) && el[2].kind == TK::NUMBER then codegen " mov eax, " + el[2].str else codegen_el el[2] end if el[0].kind != TK::ID then perror end v = @lvars[el[0].str] if v == nil then perror "undeclared variable \"" + el[0].str + "\"" end codegen " mov dword ptr [rbp - " + v[1].to_s + "], eax" end
変数の参照のコード生成
最後だよ。
# 式のコード生成(二項演算の左側被演算子) def codegen_elf(operand) if operand.kind_of?(Array) then if operand[0].size == 2 && operand[0].kind == TK::ID && operand[1].str == "()" then codegen " call " + operand[0].str else codegen_el operand end elsif operand.kind == TK::NUMBER then codegen " mov eax, " + operand.str elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end codegen " mov eax, dword ptr [rbp - " + v[1].to_s + "]" end end # 式のコード生成(二項演算の右側被演算子) def codegen_els(op, operand) if op.str == "+" then ostr = "add " elsif op.str == "-" then ostr = "sub " elsif op.str == "*" then ostr = "mul " elsif op.str == "/" then ostr = "div " elsif op.str == "==" then ostr = "cmp " else perror "unknown operator \"" + op.str + "\"" end if operand.kind_of?(Array) then if operand[0].size == 2 && operand[0].kind == TK::ID && operand[1].str == "()" then codegen " push rax" codegen " call " + operand[0].str codegen " mov ebx, eax" codegen " pop rax" else codegen " push rax" codegen_el operand codegen " mov ebx, eax" codegen " pop rax" end if op.str == "==" then codegen " " + ostr + " eax, ebx" codegen " sete al" codegen " and eax, 1" elsif op.str == "*" || op.str == "/" then codegen " mov r11, rdx" if op.str == "/" then codegen " xor edx, edx" end codegen " " + ostr + " ebx" codegen " mov rdx, r11" else codegen " " + ostr + " eax, ebx" end elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end varad = "dword ptr [rbp - " + v[1].to_s + "]" if op.str == "==" then codegen " " + ostr + " eax, " + varad codegen " sete al" codegen " and eax, 1" elsif op.str == "*" || op.str == "/" then codegen " mov ebx, " + varad codegen " mov r11, rdx" if op.str == "/" then codegen " xor edx, edx" end codegen " " + ostr + " ebx" codegen " mov rdx, r11" else codegen " " + ostr + " eax, " + varad end elsif operand.kind == TK::NUMBER then if op.str == "==" then codegen " " + ostr + " eax, " + operand.str codegen " sete al" codegen " and eax, 1" elsif op.str == "*" || op.str == "/" then codegen " mov ebx, " + operand.str codegen " mov r11, rdx" if op.str == "/" then codegen " xor edx, edx" end codegen " " + ostr + " ebx" codegen " mov rdx, r11" else codegen " " + ostr + " eax, " + operand.str end end end
これで修正は終わりかな。はあ疲れた。
動作テスト
今回は修正箇所多かったんで上手く動くか心配だな。
~/myc$ myc m6.myc ~/myc$ ./m6 Answer is 42 ~/myc$
ふう、上手くいったよ。もうちょっと複雑なのでもテストしてみる。
// 変数 int main() { int a, b; int answer; a = 6; b = 7; answer = a * b; printf("Answer is %d\n",answer); }
これも動いたよ。これ以外のパターンもいくつか試してみたよ。特にコンパイルエラーになる場合とかね。今回は修正箇所多かったからテストはもうちょっと色々やらないとダメかな。
問題点
これでローカル変数が使えるようになったんだけど、色々積み残しがあるなあ。
- スタックフレームのサイズが64バイト固定になってる
- 多重代入に対応していない
- 変数宣言時の初期化ができない
- 入子になったブロックの内側の変数のことは考慮していない
- グローバル変数は考慮していない
色々あるけどできるところからちょっとずつ片付けてくよ。