コンパイラ作成(90) ネストした関数呼出

今回の目標

double型引数の関数呼出が入子になってる場合に対応するよ。

// double型
double dadd(double a, double b)
{
    printf("dadd: %f %f\n", a, b);
    return a + b;
}

int main()
{
    double x = 12.3, y = -2.8;
    printf("%f %f %f\n", x, y, dadd(5.4, 3.2));
    printf("%d %d %f\n", 12, 7, dadd(5.4, 3.2));
}

関数daddの呼出で既にセットされてるxmmレジスタの値が潰されちゃうんで、daddコールの前後で退避・復帰する処理を追加する。昔、int型の時にも同じような処理を入れたけど、そこを改変するよ。

initialize
  # コンストラクタ
  def initialize(fname)
    @fname = fname                        # ソースファイルのファイル名
    @asmfname = fname.sub(/\.myc$/,'.s')  # アセンブリコードのファイル名
    @regs32 = ["edi", "esi","edx","ecx","r8d","r9d"]  # 32bitレジスタ
    @regs64 = ["rdi", "rsi","rdx","rcx","r8", "r9" ]  # 64bitレジスタ
    @lex = Lexer.new(@fname)              # 字句解析
    @funcname = nil                       # 現在処理している関数名
    @labelcnt = nil                       # 自動生成するラベルの個数(関数単位)
    @literalcnt = 0                       # 文字列リテラルの数
    @literaltable = []                    # 文字列リテラルのリスト
    @functions = Hash.new                 # 関数
    @lvars = nil                          # ローカル変数
    @lvarsize = nil                       # スタックに確保する領域のサイズ
    @breaklabel = nil                     # breakの飛び先のラベル
    @codebuffer = []                      # コードバッファ
    @numuseregs = 0                       # 関数コールで使用している一般レジスタの数
    @numusexmms = 0                       # 関数コールで使用しているxmmレジスタの数
    @numblock = nil                       # blockの個数
    @blocks = nil                         # ネストしたblock(["B2#","B1#",""])
  end

使用しているxmmレジスタの数用の変数を一個追加。

codegen_func
  # 関数コールのコード生成
  def codegen_func operand
    rettype = "int"
    xmm = 0    # xmmレジスタの使用数
    f = @functions[operand[0].str]
    # 既に使用されているレジスタをスタックに保存する
    if @numuseregs + @numusexmms != 0 then
      size = (@numuseregs + @numusexmms) * 8
      if size % 16 != 0 then size += 8 end
      codegen "  sub    rsp, #{size}"
      offset = 0
      (0...@numuseregs).each do |i|
        codegen "  mov    qword ptr [rsp + #{offset}], #{@regs64[i]}"
        offset += 8
      end
      (0...@numusexmms).each do |i|
        codegen "  movsd  qword ptr [rsp + #{offset}], xmm#{i}"
        offset += 8
      end
    end
    if f != nil then
      if operand.size - 2 != f[1].size then
        perror "wrong number of parameters"
      end
    end
    # 引数を順番に評価する
    (0...operand.size-2).each do |i|
      savegpr, savexmm = @numuseregs, @numusexmms
      @numuseregs, @numusexmms = i-xmm, xmm
      type = codegen_el operand[i+2]
      @numuseregs, @numusexmms = savegpr, savexmm
      if f != nil then
        if f[1][i] == "size_t" then
          codegen "  movsx rax, eax"
          type = f[1][i]
        elsif f[1][i] == "void*" && is_pointer_type?(type) then
          type = f[1][i]
        end
        if type != f[1][i] then perror "incompatible type parameter" end
      end
      if type == "int" || type == "char" then
        codegen "  mov  #{@regs32[i-xmm]}, eax"
      elsif type == "size_t"
        codegen "  mov  #{@regs64[i-xmm]}, rax"
      elsif is_pointer_type?(type) then
        codegen "  mov  #{@regs64[i-xmm]}, rax"
      elsif type == "double" then
        codegen "  movsd  xmm#{xmm}, xmm8"
        xmm += 1
      else
        perror
      end
    end
    # 関数をコール
    if f == nil then
      codegen "  mov  al, #{xmm}"
    end
    codegen "  call " + operand[0].str
    if f != nil then
      rettype = f[0]
    end
    if rettype == "double"
      codegen "  movsd  xmm8, xmm0"
    end
    # スタックに保存したレジスタを復帰する
    if @numuseregs + @numusexmms != 0 then
      size = (@numuseregs + @numusexmms) * 8
      if size % 16 != 0 then size += 8 end
      offset = 0
      (0...@numuseregs).each do |i|
        codegen "  mov    #{@regs64[i]}, qword ptr [rsp + #{offset}]"
        offset += 8
      end
      (0...@numusexmms).each do |i|
        codegen "  movsd  xmm#{i}, qword ptr [rsp + #{offset}]"
        offset += 8
      end
      codegen "  add    rsp, #{size}"
    end
    return rettype
  end

int型の時はゼネラルパーパスレジスタの退避/復帰にpush/pop使ってたんだけど、xmmレジスタはpush/popできないんでmovsd使ってるよ。で、この際なんでゼネラルパーパスレジスタの方もpush/popやめてmovにした。ちゃんと確かめてないけどこっちの方がクロック数的にお得なんじゃないかな。違うかな。

動作テスト
~/myc$ myc p25.myc
~/myc$ ./p25
dadd: 5.400000 3.200000
12.300000 -2.800000 8.600000
dadd: 5.400000 3.200000
12 7 8.600000
~/myc$

大丈夫だよね。アセンブリコードも見ておくよ。

.intel_syntax noprefix
.global dadd
dadd:
  push rbp
  mov  rbp, rsp
  sub  rsp, 16
  movsd  qword ptr [rbp - 8], xmm0
  movsd  qword ptr [rbp - 16], xmm1
  lea  rax, .L.str.0
  mov  rdi, rax
  movsd  xmm8, qword ptr [rbp - 8]
  movsd  xmm0, xmm8
  movsd  xmm8, qword ptr [rbp - 16]
  movsd  xmm1, xmm8
  mov  al, 2
  call printf
  movsd  xmm8, qword ptr [rbp - 8]
  addsd  xmm8, qword ptr [rbp - 16]
  movsd  xmm0, xmm8
.RET_dadd:
  add  rsp, 16
  pop  rbp
  ret
.global main
main:
  push rbp
  mov  rbp, rsp
  sub  rsp, 16
  movsd  xmm8, qword ptr [.L.float.1]
  movsd  qword ptr [rbp - 8], xmm8
  movsd  xmm8, qword ptr [.L.float.2]
  movq   rax, xmm8
  movabs r10, 8000000000000000H
  xor    rax, r10
  movq   xmm8, rax
  movsd  qword ptr [rbp - 16], xmm8
  lea  rax, .L.str.3
  mov  rdi, rax
  movsd  xmm8, qword ptr [rbp - 8]
  movsd  xmm0, xmm8
  movsd  xmm8, qword ptr [rbp - 16]
  movsd  xmm1, xmm8
  sub    rsp, 32
  mov    qword ptr [rsp + 0], rdi
  movsd  qword ptr [rsp + 8], xmm0
  movsd  qword ptr [rsp + 16], xmm1
  movsd  xmm8, qword ptr [.L.float.4]
  movsd  xmm0, xmm8
  movsd  xmm8, qword ptr [.L.float.5]
  movsd  xmm1, xmm8
  call dadd
  movsd  xmm8, xmm0
  mov    rdi, qword ptr [rsp + 0]
  movsd  xmm0, qword ptr [rsp + 8]
  movsd  xmm1, qword ptr [rsp + 16]
  add    rsp, 32
  movsd  xmm2, xmm8
  mov  al, 3
  call printf
  lea  rax, .L.str.6
  mov  rdi, rax
  mov  eax, 12
  mov  esi, eax
  mov  eax, 7
  mov  edx, eax
  sub    rsp, 32
  mov    qword ptr [rsp + 0], rdi
  mov    qword ptr [rsp + 8], rsi
  mov    qword ptr [rsp + 16], rdx
  movsd  xmm8, qword ptr [.L.float.7]
  movsd  xmm0, xmm8
  movsd  xmm8, qword ptr [.L.float.8]
  movsd  xmm1, xmm8
  call dadd
  movsd  xmm8, xmm0
  mov    rdi, qword ptr [rsp + 0]
  mov    rsi, qword ptr [rsp + 8]
  mov    rdx, qword ptr [rsp + 16]
  add    rsp, 32
  movsd  xmm0, xmm8
  mov  al, 1
  call printf
.RET_main:
  add  rsp, 16
  pop  rbp
  ret
.L.str.0:
  .asciz  "dadd: %f %f\n"
.L.float.1:
  .quad  402899999999999aH    # 12.3
.L.float.2:
  .quad  4006666666666666H    # 2.8
.L.str.3:
  .asciz  "%f %f %f\n"
.L.float.4:
  .quad  401599999999999aH    # 5.4
.L.float.5:
  .quad  400999999999999aH    # 3.2
.L.str.6:
  .asciz  "%d %d %f\n"
.L.float.7:
  .quad  401599999999999aH    # 5.4
.L.float.8:
  .quad  400999999999999aH    # 3.2

daddの前後でちゃんと退避・復帰されてる。昨日の夜遅い時間に考えてた時は難しく感じたんだけど、実際にやってみたらさほど難しくはなかったよ。遅い時間だと頭のクロック数が落ちてるからかな。
さて次回はなにやるかな。上のアセンブリコード見てたらリテラルが重複してるの気になるから修正しようかな。clang先輩はちゃんと一個に纏めてたよなあ。それに.L.floatってのも変だよなあ。どうして.L.doubleにしなかったんだろ。将来double型以外にfloat型をサポートするとき困るよなあ。うん、次回修正しよう。