vm2gol v2 製作メモ(19) 関数に引数を渡す


今回は関数の引数をやります。こんな感じで進めましょう。

  • 引数を1個渡す
  • 渡した引数をローカル変数に代入
  • 引数を2個渡してローカル変数に代入

引数を1個渡す

これを動かします。

// 19_func_arg.vgt.json

["stmts"

, ["func", "main"
  , []
  , [
      ["call", "fn_sub", 34]  // 引数 34 を渡す
    ]
  ]

, ["func", "fn_sub"
  , ["arg1"]  // 引数を arg1 として受け取る
  , [
    ]
  ]

]

call文を変更して、3個目以降の要素として引数を渡すようにしてみます。

["call", "{関数名}", 引数1, 引数2, ...]

アセンブリではサブルーチンに引数を渡すときどうやってたか、 ちょっとおさらい。

こんな感じでしたね。

  • call の前に引数を逆順で push して、
  • 呼び出し先のサブルーチンでは bp+N で参照する
  • サブルーチンから戻ったときに add_sp でスタックポインタを戻す

ということは、引数が1個のときは次のようなアセンブリコードに変換されてほしい。

# 略

label main
  push 34 # 引数を push
  call fn_sub
  add_sp 1 # sp を調整

label fn_sub
  # bp+N で参照

call の前の push と call の後の add_sp を 追加で出力すればよさそうです。

まずはハードコーディングで。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -27,7 +27,9 @@ def codegen_func_def(rest)
     case stmt_head
     when "call"
       fn_name = stmt_rest[0]
+      alines << "  push 34"
       alines << "  call #{fn_name}"
+      alines << "  add_sp 1"
     when "var"
       lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"

コンパイルすると期待するアセンブリコードが出力され、問題なさそうです。 単に2行追加されるだけですからね。


ではハードコーディングした部分を書き換えていきましょう。

push に渡すのは引数の内容なので、どうすればいいかというと……

["call", "fn_sub", 34] // 引数 34 を渡す

「3個目以降を引数とする」としたので、 call文の配列の 3つ目の要素を使えばいいですね。

add_sp に渡す 1 は単に引数の個数を与えてやればいいので、 特に難しくなさそうです。

修正します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -26,10 +26,10 @@ def codegen_func_def(rest)
     stmt_head, *stmt_rest = stmt
     case stmt_head
     when "call"
-      fn_name = stmt_rest[0]
-      alines << "  push 34"
+      fn_name, *fn_args = stmt_rest
+      alines << "  push #{fn_args[0]}"
       alines << "  call #{fn_name}"
-      alines << "  add_sp 1"
+      alines << "  add_sp #{fn_args.size}"
     when "var"
       lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"

run.sh で実行して確認。

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
      02   ["exit"]
      03 ["label", "main"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["push", 34]
      12   ["call", 24]
      14   ["add_sp", 1]
      16   ["cp", "bp", "sp"]
      19   ["pop", "bp"]
      21   ["ret"]
      22 ["label", "fn_sub"]
      24   ["push", "bp"]
      26   ["cp", "sp", "bp"]
pc => 29   ["cp", "bp", "sp"]
      32   ["pop", "bp"]
      34   ["ret"]
---- memory (stack) ----
         36 0
         37 0
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
sp bp => 44 47
         45 14
         46 34 ... bp+2 の位置に arg1 の値がセットされている
         47 49
         48 2
         49 0

よしよし。

渡した引数をローカル変数に代入

渡した引数は使わないと意味がありません。 次は引数を参照してローカル変数に値をセットしてみましょう。

--- a/19_func_arg.vgt.json
+++ b/19_func_arg.vgt.json
@@ -10,6 +10,8 @@
 , ["func", "fn_sub"
   , ["arg1"]  // 引数を arg1 として受け取る
   , [
+      ["var", "a"]
+    , ["set", "a", "arg1"]
     ]
   ]

とりあえず実行してみると……

$ ./run.sh 19_func_arg.vgt.json 

(略)

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
      02   ["exit"]
      03 ["label", "main"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["push", 34]
      12   ["call", 24]
      14   ["add_sp", 1]
      16   ["cp", "bp", "sp"]
      19   ["pop", "bp"]
      21   ["ret"]
      22 ["label", "fn_sub"]
      24   ["push", "bp"]
      26   ["cp", "sp", "bp"]
      29   ["sub_sp", 1]
pc => 31   ["cp", "arg1", "[bp-1]"]
      34   ["cp", "bp", "sp"]
      37   ["pop", "bp"]
      39   ["ret"]
---- memory (stack) ----
         35 0
         36 0
         37 0
         38 0
         39 0
         40 0
         41 0
         42 0
sp    => 43 0
   bp => 44 47
         45 14
         46 34
         47 49
         48 2
         49 0

vgvm.rb:235:in `copy': Not yet implemented ("copy src") ("arg1") (RuntimeError)
        from vgvm.rb:149:in `block in start'
        from vgvm.rb:126:in `loop'
        from vgvm.rb:126:in `start'
        from vgvm.rb:330:in `<main>'

ふむ……。


現時点ではこのようなアセンブリコードが出力されています。

# 略

label fn_sub
  push bp
  cp sp bp

  # 関数の処理本体
  sub_sp 1
  cp arg1 [bp-1] … 問題の箇所

  cp bp sp
  pop bp
  ret

VM(CPU)がこれを見て、 「コピー元が arg1 となってるけど arg1 ってなんやねん」 となってるわけですね。 ごもっとも。 VMちゃんにも分かる形にしてあげないと。

arg1 というのは関数呼び出しで渡された引数の名前なので、 これが [bp+2] となるように変換できればよさそうです。

ローカル変数のときは

lvar_pos = lvar_names.index(lvar_name) + 1
alines << "  cp #{src_val} [bp-#{lvar_pos}]"

のようにして [bp-N] の形にしていましたが、 同じようなやり方でいけるんじゃないでしょうか。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -10,6 +10,7 @@ def codegen_func_def(rest)
   alines = []
 
   fn_name = rest[0]
+  fn_arg_names = rest[1]
   body = rest[2]
 
   alines << ""
@@ -35,7 +36,18 @@ def codegen_func_def(rest)
       alines << "  sub_sp 1"
     when "set"
       lvar_name = stmt_rest[0]
-      val = stmt_rest[1]
+
+      val =
+        case
+        when stmt_rest[1].is_a?(Integer)
+          stmt_rest[1]
+        when fn_arg_names.include?(stmt_rest[1])
+          fn_arg_pos = fn_arg_names.index(stmt_rest[1]) + 2
+          "[bp+#{fn_arg_pos}]"
+        else
+          raise not_yet_impl("set val", stmt_rest)
+        end
+
       lvar_pos = lvar_names.index(lvar_name) + 1
       alines << "  cp #{val} [bp-#{lvar_pos}]"
     else

動かしてみます。

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
      02   ["exit"]
      03 ["label", "main"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["push", 34]
      12   ["call", 24]
      14   ["add_sp", 1]
      16   ["cp", "bp", "sp"]
      19   ["pop", "bp"]
      21   ["ret"]
      22 ["label", "fn_sub"]
      24   ["push", "bp"]
      26   ["cp", "sp", "bp"]
      29   ["sub_sp", 1]
      31   ["cp", "[bp+2]", "[bp-1]"]
pc => 34   ["cp", "bp", "sp"]
      37   ["pop", "bp"]
      39   ["ret"]
---- memory (stack) ----
         35 0
         36 0
         37 0
         38 0
         39 0
         40 0
         41 0
         42 0
sp    => 43 34 ... [bp-1] ローカル変数 a
   bp => 44 47
         45 14
         46 34 ... [bp+2] 引数 arg1
         47 49
         48 2
         49 0

["set", "a", "arg1"] を実行した直後の状態です。 うまく動いてますね。


「実際のコンピュータでもメモリからメモリへのコピーってできるんだっけ?」 というとこが気になりますが、 ここはいったんできるということにして進めます。 あとで確認するかも。

なんとなれば、レジスタを経由させて

cp arg1 reg_a
cp reg_a a

とすれば回避できそうなどと考えつつ、とにかく進めます。

引数を2個渡す

さて、引数の参照部分もクリアできたので、 2個以上の引数でも動くようにしましょう。

vgtコードはこう。

// 19_func_args.vgt.json

["stmts"

, ["func", "main"
  , []
  , [
      ["call", "fn_sub", 34, 56] // 引数 34, 56 を渡す
    ]
  ]

, ["func", "fn_sub"
  , ["arg1", "arg2"] // 引数を arg1, arg2 として受け取る
  , [
      ["var", "a"]
    , ["set", "a", "arg1"]
    , ["var", "b"]
    , ["set", "b", "arg2"]
    ]
  ]

]

call文を変換している部分が今どうなっているかというと、

    when "call"
      fn_name, *fn_args = stmt_rest
      alines << "  push #{fn_args[0]}"
      alines << "  call #{fn_name}"
      alines << "  add_sp #{fn_args.size}"

こうなっていて、 fn_args[0] だけを push しています。 ここを複数にしてやればよくて、ただし逆順に push する点に気をつける、と。

修正します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -28,7 +28,9 @@ def codegen_func_def(rest)
     case stmt_head
     when "call"
       fn_name, *fn_args = stmt_rest
-      alines << "  push #{fn_args[0]}"
+      fn_args.reverse.each {|fn_arg|
+        alines << "  push #{fn_arg}"
+      }
       alines << "  call #{fn_name}"
       alines << "  add_sp #{fn_args.size}"
     when "var"

これだけですね。動かします。

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
      02   ["exit"]
      03 ["label", "main"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["push", 56]
      12   ["push", 34]
      14   ["call", 26]
      16   ["add_sp", 2]
      18   ["cp", "bp", "sp"]
      21   ["pop", "bp"]
      23   ["ret"]
      24 ["label", "fn_sub"]
      26   ["push", "bp"]
      28   ["cp", "sp", "bp"]
      31   ["sub_sp", 1]
      33   ["cp", "[bp+2]", "[bp-1]"]
      36   ["sub_sp", 1]
      38   ["cp", "[bp+3]", "[bp-2]"]
pc => 41   ["cp", "bp", "sp"]
      44   ["pop", "bp"]
      46   ["ret"]
---- memory (stack) ----
         33 0
         34 0
         35 0
         36 0
         37 0
         38 0
         39 0
         40 0
sp    => 41 56 ... b
         42 34 ... a
   bp => 43 47
         44 16
         45 34 ... arg1
         46 56 ... arg2
         47 49
         48 2
         49 0

["set", "b", "arg2"] を実行した直後。 いいですね!

というわけで今回は引数渡しと、呼び出し先での参照ができるようになりました!!