コンパイラ作成(56) char*型+int型の式

今回の目標

型をミックスした式のサポートを頑張るよ。

// char* + int
int main()
{
    int i;
    for(i = 0; i < 10; i = i + 1)
        puts("Hello, World!" + i);
}

前々回、char*型をサポートしたけど、対応が不十分なとこがいくつもあった。で、今回少しだけ改善するよ。
それと今回の目標とは関係ないんだけど、おまけで2件修正するよ。

print、puts

まずはおまけその1。今までprint、putsはstatementメソッドの中で特別扱いしてたんだけど、もうこの処理要らなくなったんで削除することにした。

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

予約語から削除。普通の関数として扱うよ。printfに関してはまだ特別扱いのまま。printfは可変長引数なんだけど、exprにはまだこの処理が入ってないからね。

=begin
    elsif kind == TK::RESERVE && str == "print" then
      # 組み込み関数printの処理
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != "(" then perror end
      kind, str = @lex.gettoken
      kind, str = expr kind, str
      codegen "  mov edi, eax"
      codegen "  call print"
      if kind != TK::SYMBOL || str != ")" then perror end
      kind, str = @lex.gettoken
      if kind != TK::SYMBOL || str != ";" then
        perror "expected ';' after builtin function"
      end
    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
=end

statementから該当の処理を削除。printに関してはかなり初期に作ったんだよなあ。感慨深いものがあるよ。

エラーメッセージ

おまけその2。

int sub(int 123)
{
}

これをコンパイルしたときのメッセージをデフォルトから分かり易いものに変更した。

    # 引数の処理
    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 "wrong parameter name" 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

functionメソッドの引数の処理のところ。

コード生成部

それでは本題に行くよ。

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

  # 式のコード生成(二項演算の右側被演算子)
  def codegen_els(op, operand, type_l)
    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

    # 右被演算子を評価
    type_r = "int"
    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"
        type_r = 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
      type_r = v[0]
      if type_r == "char*"
        str = "qword ptr [rbp - " + v[1].to_s + "]"
      else
        str = "dword ptr [rbp - " + v[1].to_s + "]"
      end
    elsif operand.kind == TK::NUMBER then
      str = operand.str
    elsif operand.kind == TK::STRING then
      type_r = "char*"
      label = addliteral operand.str
      codegen "  lea  r10, "+label
      str = "r10"
    else
      perror
    end

    # 型チェック
    if type_l != type_r then
      if type_l == "char*" && type_r == "int" then
        if op.str != "+" && op.str != "-" then
          perror "mismatched types to binary operation"
        end
      elsif type_l == "int" && type_r == "char*" then
        if op.str != "+" && op.str != "-" then
          perror "mismatched types to binary operation"
        end
      else
        perror "mismatched types to binary operation"
      end
    elsif type_l == "char*" then
      perror "mismatched types to binary operation"
    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 "  cdq"
      end
      codegen "  " + ostr + " r10d"
      codegen "  mov  rdx, r11"
    else
      if type_l == "char*" && type_r == "int" then
        if str == op.str then
          codegen "  " + ostr + " rax, " + str
        elsif str == "r10d" then
          codegen "  movsx r10, r10d"
          codegen "  " + ostr + " rax, r10"
        else
          codegen "  mov  r10d, " + str
          codegen "  movsx r10, r10d"
          codegen "  " + ostr + " rax, r10"
        end
      elsif type_l == "int" && type_r == "char*" then
        codegen "  movsx rax, eax"
        codegen "  " + ostr + " rax, " + str
        type_l = "char*"
      else
        codegen "  " + ostr + " eax, " + str
       end
    end
    return type_l
  end

前々回手抜きしたcodegen_elsも型を意識しながら、コード生成するようにしたよ。まず計算式が有効かどうかをチェックしてる。その後、左側被演算子の型と右側被演算子の型を見比べながら、movsx使ったりして計算してるよ。まだ論理に色々と不足があるよ。比較演算に対応してないし、ポインタ同士の引算にも対応してないよ。対応してるのはchar*型とint型の加減算だけ。if( p != 0 )とかは必須だからこの辺は早めに対応したいなあ。

動作テスト

まずはおまけから。

~/myc$ myc i6.myc print.o
~/myc$ ./i6
97
18048
~/myc$

print関数はちゃんと動いてるよ。

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

puts関数も大丈夫だね。

~/myc$ myc err24.myc
err24.myc:1:12 error: wrong parameter name
~/myc$ 

コンパイルエラーも分かり易いものになったよ。次は本題。

~/myc$ myc o10.myc
~/myc$ ./o10
Hello, World!
ello, World!
llo, World!
lo, World!
o, World!
, World!
 World!
World!
orld!
rld!
~/myc$ 

おお、動いたよ。演算の仕方をちょっと変えてみる。

// char* + int
int main()
{
    sub("Hello, World!");
}

int sub(char *s) {
    int i;
    for(i = 0; i < 10; i = i + 1)
        puts(s + i);
    for(i = 9; i >= 0; i = i - 1)
        puts(i + s);
}

文字列リテラルからポインタ変数というかポインタ引数にしてみたよ。

~/myc$ myc o12.myc
~/myc$ ./o12
Hello, World!
ello, World!
llo, World!
lo, World!
o, World!
, World!
 World!
World!
orld!
rld!
rld!
orld!
World!
 World!
, World!
o, World!
lo, World!
llo, World!
ello, World!
Hello, World!
~/myc$ 

これも上手くいった。次はコンパイルエラーになる場合。

// 無効な型の計算
int main()
{
    int i;
    for(i = 0; i < 10; i = i +1)
        puts("apple" + "pen");
}

C言語だからapplepenにはならないよ。

~/myc$ myc err26.myc
err26.myc:6:30 error: mismatched types to binary operation
~/myc$

ちゃんとエラーになってる。これで型をミックスした計算の初歩ができるようになったよ。今回の変更をもうちょっと頑張れば任意の型のポインタとの計算ができるようになるのかな。でもその前にやらなきゃいけないこと多いな。char型とかサポートできてないし、そもそもint型以外の変数宣言できないもんなあ。そういや’A'の文字リテラルも扱えないよなあ。あと、char型ってsignedなのかunsignedなのかって問題もあるよなあ。clang先輩はどうだっけ?うーむ、悩ましいなあ。
さて次回は何にするかな。いつも通り簡単にできそうなところから攻めていくか。