コンパイラ作成(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言語は変数の宣言が自由なんで、関数を最後まで見てかないと確定しないんだよね。どうすれば良いのか少し考えてみたよ。

  1. 2パス以上のコンパイラにする
  2. 一旦sub rsp,xxtでコード生成しておいて、関数の構文解析が終わった時点で書き換える
  3. コード生成を変更して中間言語を間に挟むようにする

モダンなコンパイラは大体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);
}

これも動いたよ。これ以外のパターンもいくつか試してみたよ。特にコンパイルエラーになる場合とかね。今回は修正箇所多かったからテストはもうちょっと色々やらないとダメかな。

問題点

これでローカル変数が使えるようになったんだけど、色々積み残しがあるなあ。

  1. スタックフレームのサイズが64バイト固定になってる
  2. 多重代入に対応していない
  3. 変数宣言時の初期化ができない
  4. 入子になったブロックの内側の変数のことは考慮していない
  5. グローバル変数は考慮していない

色々あるけどできるところからちょっとずつ片付けてくよ。