コンパイラ作成(52) 単項演算子
今回の目標
単項演算子の"+"と"-"を追加するよ。"+"の方はめったに使われないよね。
// 単行演算子 int main() { int a = 10; int b = -a; printf("a = %d\n",a); printf("b = %d\n",b); }
それとこの機会に今までちゃんとしてなかった負数の計算を修正するよ。そういや単項演算子なかったけど今まででも5-10とかで負数は作れてたんだよなあ。チェックしてなかったけど、たぶん計算結果が間違ってたのかな。
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_unaryop el, ["+","-"] el = modify_el el, ["*","/"] el = modify_el el, ["+","-"] el = modify_el el, ["<",">","<=",">="] el = modify_el el, ["==","!="] el = modify_el el, ["="], :r_to_l puts to_str(el) if $opt_d # デバッグ用 codegen_el el return kind, str end
modify_el_unaryopを追加。二項演算子の追加はmodify_elを増やすだけで済むけど、単項演算子はそれじゃ対応できないから新しいメソッドを追加したよ。
modify_el_unaryop
でこれが追加したメソッド。
# elの変形(単行演算子の処理) # [-, 2, +, 3] # =>[[-, 2], + 3] # [-, 2, +, -, 3] # =>[[-, 2], + [-, 3]] def modify_el_unaryop(el, opl) mel = [] prev = nil loop do if el.size == 0 then break end x = el.shift if ((prev == nil) || (!prev.kind_of?(Array) && (prev.kind == TK::SYMBOL))) \ && !x.kind_of?(Array) && x.kind == TK::SYMBOL then if opl.include? x.str then tel = [] tel << x x = el.shift if x.kind_of?(Array) then x = modify_el_unaryop x, opl end tel << x mel << tel prev = tel else perror end else if x.kind_of?(Array) then x = modify_el_unaryop x, opl end mel << x prev = x end end return mel end
elをスキャンして単項演算子だったら[]で括ってってる。この処理だと単項演算子が連続した場合ちゃんと扱えない問題があるんで将来見直すつもり。a=-*p;みたいな場合ね。(正直に言うとこの記事書きながらコード見てて気が付いたよ)
コード生成
次はコード生成部の修正。
# 式のコード生成 def codegen_el(el) if !el[0].kind_of?(Array) && el[0].kind == TK::SYMBOL codegen_unaryop el[0], [el[1]] elsif (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_unaryop(op, operand) codegen_el operand if op.str == "-" then codegen " neg eax" end end
単項演算子の場合はcodegen_unaryopを呼ぶようにしたよ。将来、単項演算子をもっとサポートするとコードが膨らみそうなんで分けた。
codegen_els
マイナスの数の計算が正しくできるようにする修正。
# 式のコード生成(二項演算の右側被演算子) def codegen_els(op, operand) if op.str == "+" then ostr = "add " elsif op.str == "-" then ostr = "sub " elsif op.str == "*" then ostr = "imul" elsif op.str == "/" then ostr = "idiv" elsif op.str == "==" then ostr = "cmp " elsif op.str == "!=" then ostr = "cmp " elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || 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 " sub rsp, 8" codegen " push rax" codegen_func operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" else codegen " sub rsp, 8" codegen " push rax" codegen_el operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" end str = "r10d" elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end str = "dword ptr [rbp - " + v[1].to_s + "]" elsif operand.kind == TK::NUMBER then str = operand.str end if op.str == "==" then codegen " " + ostr + " eax, " + str codegen " sete al" codegen " and eax, 1" elsif op.str == "!=" then codegen " " + ostr + " eax, " + str codegen " setne al" codegen " and eax, 1" elsif op.str == "<" then codegen " " + ostr + " eax, " + str codegen " setl al" codegen " and eax, 1" elsif op.str == ">" then codegen " " + ostr + " eax, " + str codegen " setg al" codegen " and eax, 1" elsif op.str == "<=" then codegen " " + ostr + " eax, " + str codegen " setle al" codegen " and eax, 1" elsif op.str == ">=" then codegen " " + ostr + " eax, " + str codegen " setge al" codegen " and eax, 1" elsif op.str == "*" || op.str == "/" then if str != "r10d" then codegen " mov r10d, " + str end codegen " mov r11, rdx" if op.str == "/" then # codegen " xor edx, edx" codegen " cdq" end codegen " " + ostr + " r10d" codegen " mov rdx, r11" else codegen " " + ostr + " eax, " + str end end
mul/divをimul/idivに変更。それと割り算の時、符号拡張するためにcdqに変更したよ。
動作テスト
それじゃ行くよ。例の俺以外が見ても理解できないだろうデバッグ情報付きでやってみる。
~/myc$ myc -d i20.myc var a [a, =, 10] [[a, =, 10]] var b [b, =, -, a] [[b, =, [-, a]]] [a] [a] [b] [b] {"a"=>["int", 4], "b"=>["int", 8]} {"main"=>["int", []]} ~/myc$ ./i20 a = 10 b = -10 ~/myc$
上手くいったみたい。この他にもいくつかの場合をテストしたけど、テストの抜けがあるかも。mycもちょっとずつ進化してきてできること増えてきたんだけど、その分テストしなきゃいけない項目も増えてきてる。ちゃんと考えて一個一個テストしないといけないけどこれが結構大変だよ。
それと、負数の計算がちゃんとできてるかのチェックが不十分だよなあ。今回はここまでにするけど、次回もう少しテストをするよ。