コンパイラ作成(54) 文字列定数が引数の関数

今回の目標

今回は引数がint型じゃない関数に対応するよ。

// 文字列の引数
int main()
{
    puts_x("Hello, World!", 5);
}

int puts_x(char *s, int n)
{
    int i;
    for(i = 0; i < n; i = i + 1)
        puts(s);
}

いきなりchar型、それも*付き。C言語初心者を地獄に突き落とすポインタだよ。今回の目標は結構ハードルが高い。一回できれいに飛ぶのは難しんであちこち手抜きで進めるよ。

perror

今回の目標とは関係ないんだけどちょっと修正するよ。

  # コンパイルエラーを表示
  def perror(emsg = "syntax error" )
    fname, lineno, pos = @lex.position
    print fname,":",lineno,":",pos," error: ", emsg, "\n"
    caller.each do |f| puts f end if $opt_d
    exit -1;    # コンパイル処理を打ち切る
  end

デバッグモードの時にどっからコールされてるかを表示するようにしたよ。なくても何とかなるんだけど、これがあった方がデバッグ効率上がると思うんで入れてみた。

Lexer

トークンの種類を一つ増やした。

# トークンの種類 enumの代わり
module TK
  EOF     = 1    # End of file
  ID      = 2    # Identifier
  NUMBER  = 3    # Number
  SYMBOL  = 4    # Symbol
  STRING  = 5    # String
  RESERVE = 6    # Reserved word
  TYPE    = 7    # Type word
  UNKNOWN = 8    # Unkown token
end

RESERVEからTYPEを分離。

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

int、charはTK::TYPE。

      # identifireの切り出し
      if m = @line[@idx,@line.length].match(/^[\p{alpha}|_][\p{alnum}|_]*/) then
        str = m.to_s
        @idx += str.length
        if @reservedword.include?(str) then
          return TK::RESERVE, str
        elsif @typeword.include?(str) then
          return TK::TYPE, str
        else
          return TK::ID, str
        end

gettokenの該当箇所を修正。

  # モジュールテスト
  def moduletest()
    i = 0
    loop do
      i += 1
      kind, str = gettoken
#     fname, lineno, pos = position
#     print fname,":",lineno,":",pos," "
      if kind == TK::EOF then
        break
      elsif kind == TK::ID      then
        print "TK::ID      ",str,"\n"
      elsif kind == TK::NUMBER  then
        print "TK::NUMBER  ",str,"\n"
      elsif kind == TK::STRING  then
        print "TK::STRING  ",str,"\n"
      elsif kind == TK::SYMBOL  then
        print "TK::SYMBOL  ",str,"\n"
      elsif kind == TK::RESERVE then
        print "TK::RESERVE ",str,"\n"
      elsif kind == TK::TYPE    then
        print "TK::TYPE    ",str,"\n"
      elsif kind == TK::UNKNOWN then
        print "TK::UNKNOWN ",str,"\n"
        break
      end
    end
  end

ここも忘れずに修正。

function

次は関数定義を処理するここを修正。

  # 関数の構文解析
  def function()
    @labelcnt = 0
    @lvars = Hash.new
    @lvarsize = 0;
    rettype = nil
    kind, str = @lex.gettoken
    if kind == TK::TYPE && 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
    parametersize = []
    kind, str = @lex.gettoken
    if kind != TK::SYMBOL || str != "(" then perror end
    kind, str = @lex.gettoken
    loop do
      if kind == TK::SYMBOL && str == ")" then break end
      if kind == TK::TYPE then
        type = str
        kind, str = @lex.gettoken
        if kind == TK::SYMBOL && str == "*" then
          type += str
          kind, str = @lex.gettoken
        end
        if kind != TK::ID then perror end
        print "para "+str+"\n" if $opt_d
        size = sizeof type
        @lvarsize += size
        parametersize << size
        if @lvars[str] then perror "redefinition parameter \"" + str +"\"" end
        @lvars[str] = [type,@lvarsize]
      else
        perror
      end
      kind, str = @lex.gettoken
      if kind == TK::SYMBOL && str == "," then
        kind, str = @lex.gettoken
      end
    end
    codegen ".global "+@funcname
    codegen @funcname+":"
    codegen "  push rbp"
    codegen "  mov  rbp, rsp"
    # 仮のコードを作成
    idx = codegen "  sub  rsp, xx"
    n = @lvars.size
    offset = 0
    (0...n).each do |i|
      offset += parametersize[i]
      if parametersize[i] == 4 then
        codegen "  mov  dword ptr [rbp - #{offset}], #{@regs32[i]}"
      else
        codegen "  mov  qword ptr [rbp - #{offset}], #{@regs64[i]}"
      end
    end
    kind, str = @lex.gettoken
    if kind != TK::SYMBOL || str != "{" then perror end
    kind, str = block
    codegen ".RET_" + @funcname + ":"
    # 16の倍数になるように揃える
    size = (@lvarsize+15) / 16 * 16
    # 正しいサイズでコードを生成し置き換える
    if size != 0 then
      codechange idx,"  sub  rsp, #{size}"
      codegen "  add  rsp, #{size}"
    else
      codechange idx,nil
    end
    codegen "  pop  rbp"
    codegen "  ret"
    if @functions[@funcname] != nil then perror "redefinition of \"" + @funcname + "\"" end
    @functions[@funcname] = [rettype,[]]
    p @lvars if $opt_d   # デバッグ用
    optimize if $opt_O != 0
    codeflush
    @funcname = nil
    return true
  end

intがTK::TYPEになったことに対応。引数の型によってバイト数が違うんでその関連の処理が増えてるよ。新しいメソッド、sizeofで指定されてる型に必要なバイト数を求めてる。引数をレジスタからスタック上へ移す処理も型によって分けるようにした。
このメソッド最初は単純だったのに大分ごちゃごちゃして分かり辛くなってきたなあ。

sizeof

でこれが追加したメソッド。

  # 型のサイズ
  def sizeof(type)
    case type
      when "int"
        return 4
      when "char*"
        return 8
      else
        perror "unknown type"
      end
  end

手抜きでint型とchar*型にしか対応してないよ。

statement

次はstatementメソッドの修正。

    elsif kind == TK::TYPE && 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]
        skind, sstr = @lex.gettoken
        if skind == TK::SYMBOL && sstr == "=" then
          kind, str = expr2 kind, str, skind, sstr;
        else
          kind, str = skind, sstr;
        end
        if kind != TK::SYMBOL || str != "," then break end
      end
      if kind != TK::SYMBOL || str != ";" then
        perror "expected ';' after variables"
      end

ここはTK::TYPE関連の修正だけ。char型の変数宣言とかには対応してないよ。

    elsif kind == TK::RESERVE && str == "puts" then
      # 標準関数putsの処理
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != "(" then perror end
      kind, str = @lex.gettoken
      if kind == TK::STRING then
        label = addliteral str
        codegen "  lea rdi, "+label
      elsif kind == TK::ID then
        v = @lvars[str]
        if v == nil then
          perror "undeclared variable \"" + str + "\""
        end
        codegen "  mov  rdi, qword ptr [rbp - " + v[1].to_s + "]"
      else
        perror
      end
      codegen "  call puts"
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != ")" then perror end
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != ";" then
        perror "expected ';' after function"
      end

今までputs("string")の形式にしか対応してなかったけど、puts(s)にも対応したよ。えーと、そもそももうこの処理自体いらないのかな。引数付きの関数コールはexprの中で処理できるようになってるし、今回文字列定数にも対応するから、全部exprに任せて大丈夫なはずだよなあ。今回は変更箇所多いからテスト項目を増やしたくないんで、このまま残すことにするよ。

コード生成

ここが今回一番悩んだ場所。今まで全部int型だったけど、char*型にも対応しないとダメなんで、codegen_el以下のインターフェースを変更することにしたよ。

  # 式のコード生成
  def codegen_el(el)
    type = "int"
    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
      type = codegen_elf el.shift
      if type != "int" && el != [] then
        perror
      end
      loop do
        if el == [] then break end
        op = el.shift
        operand = el.shift
        codegen_els op, operand
      end
    end
    return type
  end

戻り値で型情報を返すようにした。型をミックスした式はちゃんとチェックして無効な時はエラーにしないとダメなんだけどその辺未対応だよ。

  # 式のコード生成(二項演算の左側被演算子)
  def codegen_elf(operand)
    type = "int"
    if operand.kind_of?(Array) then
      if !operand[0].kind_of?(Array) && operand[0].kind == TK::ID && operand[1].str == "()" then
        codegen_func operand
      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 + "]"
    elsif operand.kind == TK::STRING then
      type = "char*"
      label = addliteral operand.str
      codegen "  lea  rax, "+label
    else
      perror
    end
    return type
  end

ここも型情報を返すようにした。それと文字列定数の時の処理を追加。codegen_elsとかも変更す必要があるんだけど、今回は見なかったことにして先に進むよ。

  # 関数コールのコード生成
  def codegen_func operand
    if @numuseregs != 0 then
      if @numuseregs % 2 == 1 then codegen "  sub  rsp, 8" end
      (0...@numuseregs).each do |i| codegen "  push #{@regs64[i]}" end
    end
    (0...operand.size-2).each do |i|
      save = @numuseregs
      @numuseregs = i
      type = codegen_el operand[i+2]
      @numuseregs = save
      if type == "int"
        codegen "  mov  #{@regs32[i]}, eax"
      elsif type == "char*"
        codegen "  mov  #{@regs64[i]}, rax"
      else
        perror
      end
    end
    codegen "  call " + operand[0].str
    if @numuseregs != 0 then
      (0...@numuseregs).reverse_each do |i| codegen "  pop  #{@regs64[i]}" end
      if @numuseregs % 2 == 1 then codegen "  add  rsp, 8" end
    end
  end

codegen_elの戻り値を見てeaxかraxを判断してる。

動作テスト

ではテストしてみるよ。

~/myc$ myc -d err1.myc
[42]
[42]
err1.myc:3:14 error: expected ';' after return statement
/usr/local/bin/myc:767:in `statement'
/usr/local/bin/myc:1022:in `block in block'
/usr/local/bin/myc:1021:in `loop'
/usr/local/bin/myc:1021:in `block'
/usr/local/bin/myc:1091:in `function'
/usr/local/bin/myc:1123:in `program'
/usr/local/bin/myc:1179:in `block in <main>'
/usr/local/bin/myc:1173:in `each'
/usr/local/bin/myc:1173:in `<main>'
~/myc$ 

まずはperrorのコール元の表示をテスト。こんな感じで表示されるよ。

~/myc$ myc -l o9.myc
TK::TYPE    int
TK::ID      main
TK::SYMBOL  (
TK::SYMBOL  )
TK::SYMBOL  {
TK::ID      puts_x
TK::SYMBOL  (
TK::STRING  Hello, World!
TK::SYMBOL  ,
TK::NUMBER  5
TK::SYMBOL  )
TK::SYMBOL  ;
TK::SYMBOL  }
TK::TYPE    int
TK::ID      puts_x
TK::SYMBOL  (
TK::TYPE    char
TK::SYMBOL  *
TK::ID      s
TK::SYMBOL  ,
TK::TYPE    int
TK::ID      n
TK::SYMBOL  )
TK::SYMBOL  {
TK::TYPE    int
TK::ID      i
TK::SYMBOL  ;
TK::RESERVE for
TK::SYMBOL  (
TK::ID      i
TK::SYMBOL  =
TK::NUMBER  0
TK::SYMBOL  ;
TK::ID      i
TK::SYMBOL  <
TK::ID      n
TK::SYMBOL  ;
TK::ID      i
TK::SYMBOL  =
TK::ID      i
TK::SYMBOL  +
TK::NUMBER  1
TK::SYMBOL  )
TK::RESERVE puts
TK::SYMBOL  (
TK::ID      s
TK::SYMBOL  )
TK::SYMBOL  ;
TK::SYMBOL  }
~/myc$ 

Lexerのテスト。ちゃんとTK::TYPEになってるよ。

~/myc$ myc -d o9.myc
[[puts_x, (), [Hello, World!], [5]]]
[[puts_x, (), [Hello, World!], [5]]]
{}
para s
para n
var i
[i, =, 0]
[[i, =, 0]]
[i, <, n]
[[i, <, n]]
[i, =, i, +, 1]
[[i, =, [i, +, 1]]]
{"s"=>["char*", 8], "n"=>["int", 12], "i"=>["int", 16]}
{"main"=>["int", []], "puts_x"=>["int", []]}
~/myc$ ./o9
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
~/myc$

おお、ちゃんと動いた。今回の修正で型を意識したコード生成処理をいれたけど、あちこち手抜きになってるよ。その辺は次回以降ちょっとずつ頑張るよ。それと今回はコーディングに時間とられたんでテストが不十分だな。一応、testmycを走らせて過去のテストプログラムがコンパイル、実行できることは確かめてるけどね。
しかし新しい機能を追加するたびに積み残しがごっそり増えてくなあ。もう何が積み残されてるか把握できなくなってきたよ。まずいな、これ。