コンパイラ作成(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もちょっとずつ進化してきてできること増えてきたんだけど、その分テストしなきゃいけない項目も増えてきてる。ちゃんと考えて一個一個テストしないといけないけどこれが結構大変だよ。
それと、負数の計算がちゃんとできてるかのチェックが不十分だよなあ。今回はここまでにするけど、次回もう少しテストをするよ。