コンパイラ作成(28) printf

今回の目標

前回ABIについて調べなおしたんで、それを活かしてprintfに挑戦してみるよ。

int main()
{
    printf("Hello, World!\n");
}

printf版のハロワ。

Lexerクラス

まずLexerを二箇所修正。

    @reservedword = [
      "return","goto","if","else","for","while","until","do",
      "int","char",
      "print","puts","printf"
    ]

予約語にprintfを追加。

      # symbolの切り出し
      elsif m = @line[@idx,@line.length].match(/^[==|=|\+|\-|\*|\/|:|,|;|\(|\)|\{|\}]/)  then
        str = m.to_s
        @idx += str.length
        return TK::SYMBOL, str
      end

カンマを追加。この辺はいつもの修正だね。

statement

ここが肝心なとこ。

    elsif kind == TK::RESERVE && str == "printf" then
      # 標準関数printfの処理
      regs32 = ["edi", "esi","edx","ecx","r8d","r9d"]
      regs64 = ["rdi", "rsi","rdx","rcx","r8", "r9" ]
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != "(" then perror end
      kind, str = @lex.gettoken
      i = 0
      loop do
        if kind == TK::STRING then
          label = addliteral str
          codegen "  lea  "+regs64[i]+", "+label
          kind, str = @lex.gettoken
        else
          kind, str = expr kind, str
          codegen "  mov  "+regs32[i]+", eax"
        end
        if kind == TK::SYMBOL && str == ")" then break end
        if kind != TK::SYMBOL || str != "," then perror end
        kind, str = @lex.gettoken
        i += 1
      end
      codegen "  mov  al, 0"
      codegen "  call printf"
      kind, str = @lex.gettoken
      if kind == TK::SYMBOL && str == ";" then return true end
      perror "expected ';' after function"
      return true;

printfに対応する処理を追加した。今回の修正はかなり安易で色々問題があるよ。まず、本来は式の処理として文字列リテラルを扱うべきなんだけど、別個に処理しちゃってること。exprの中で処理するためには、型を意識しながらコード生成しなきゃいけないね。うーん、面倒くさそう。二点目は引数6個までしか対応してないこと。7個目からはスタックに積んでいかなきゃなんないよ。三点目はprintfの呼び出し自体をexprの中でやるべきなこと。exprには既に関数コールの処理が入ってるからそれを引数に対応させれば良いんだよなあ。そうすればprintf以外の関数も呼べるようになるしね。この辺はおいおいやっていくつもり。ああ、宿題が溜まってく。

read_el

関数の引数に対応するためにちょっとだけ修正。

  # 式の最後までのトークンを読み込む
  #   返り値
  #     el   : expression's token list
  #     kind : 処理しなかったトークン(セミコロンもしくは閉じ括弧)
  #     str  : 処理しなかったトークン(セミコロンもしくは閉じ括弧)
  def read_el(fkind,fstr,skind,sstr)
    el = []
    if fkind == TK::ID && sstr == "(" then
      # 関数呼出の処理
      sel = []
      sel << Token.new(fkind,fstr)
      sel << Token.new(skind,"()")
      el << sel
      skind, sstr = @lex.gettoken
      if skind != TK::SYMBOL || sstr != ")" then perror end
      skind, sstr = @lex.gettoken
    elsif fstr == "(" then
      # 括弧の処理
      fkind, fstr = skind, sstr
      skind, sstr = @lex.gettoken
      sel, skind, sstr = read_el fkind, fstr, skind, sstr
      el << sel
      skind, sstr = @lex.gettoken
    else
      el << Token.new(fkind,fstr)
    end
    loop do
      if skind == TK::EOF then break end
      if sstr == ";" then break end
      if sstr == ")" then break end
      if sstr == "," then break end
      if sstr == "}" then break end
      fkind, fstr = skind, sstr
      skind, sstr = @lex.gettoken
      if fkind == TK::ID && sstr == "(" then
        # 関数呼出の処理
        sel = []
        sel << Token.new(fkind,fstr)
        sel << Token.new(skind,"()")
        el << sel
        skind, sstr = @lex.gettoken
        if skind != TK::SYMBOL || sstr != ")" then perror end
        skind, sstr = @lex.gettoken
      elsif fstr == "(" then
        # 括弧の処理
        fkind, fstr = skind, sstr
        skind, sstr = @lex.gettoken
        sel, skind, sstr = read_el fkind, fstr, skind, sstr
        el << sel
        skind, sstr = @lex.gettoken
      else
        el << Token.new(fkind,fstr)
      end
    end
    return el, skind, sstr
  end

引数のセパレータのカンマを意識するようにしたよ。ただこの修正だと将来カンマ演算子をサポートするとき問題になるね。通常は演算子扱い、引数の時はセパレータ扱いにするのは簡単じゃないなあ。C言語の仕様はいやらしいなあ。

動作テスト

それじゃ行ってみるよ。

~/myc$ myc m.myc
~/myc$ ./m
Hello, World!
~/myc$

おお、できたよ。もうちょっと複雑なのもテストしてみるよ。

int main()
{
    printf("Hello, World!\n");
    printf("%s\n","Hello, World!");
    printf("%d %d %d\n",1,2,3);
    printf("%d %d %d %d %d\n",1,2,3,4,5);
    printf("%d %d %d %d %d\n",1,2,3,4,2*5);
}

どうかな。

~/myc$ myc m3.myc
~/myc$ ./m3
Hello, World!
Hello, World!
1 2 3
1 2 3 4 5
1 0 3 4 10
~/myc$

ああ、やっぱり駄目か。最後のprintf間違ってるね。edxに入れた数値が、2*5の計算の時に潰されてる。まあこうなると予想はついてたんだけどね。修正は簡単にできると思うけど、これも宿題にして今日はここまでにしよう。