vm2gol v2 製作メモ(17) main から別の関数を呼ぶ / call文



空の main 関数の実行の次は、 main から 別の関数の呼び出しをやってみましょう。

簡単そうですが、これも一歩ずつということで 2段階に分けます。

  • 関数定義を並べるだけ(main からの呼び出しはしない)
  • main から呼び出す

やっていきましょう。

関数定義を並べるだけ

まずは関数定義を並べるだけ。 vgt のコードはこんな感じにしました。

// 17_empty_main_sub.vgt.json

["stmts"

, ["func", "main"
  , []
  , []
  ]

, ["func", "fn_sub"
  , []
  , []
  ]

]

トップレベルというかルート要素的なものは1つにした方がいいかな? となんとなく考えて、

[
  "stmts"
, {文1}
, {文2}
, ...
, {文n}
]

という構文を考えてみました。 stmts は statements の略です。 この構文になっていたら、文1, 文2, ... 文n と順番に文を実行していく、というものです。 Lisp だと progn でしょうか。

( 関数の定義は文なのか? というのはちょっと気になるところですが、 とりあえず文であるということにしておきます。 )

これをコンパイルしたらこんなアセンブリコードができてほしい。

  call main
  exit

label main
  ...

label fn_sub
  ...

では、コンパイラというかコード生成部分を修正していきます。 stmts の 文1, 文2, ..., 文n を順番に見ていって label 〜 の形で並べればいいだけなので これは全然むずかしくなさそう。


修正の前にまずはリファクタリングします。 codegen() に直接書いていた、 「関数定義に対応するアセンブリコードに変換する処理」の部分(ややこしいですね……) をメソッド抽出しておきます。

diff が分かりやすくならないので リファクタリング後のコードを貼ります。

def codegen_func_def(rest)
  alines = []

  fn_name = rest[0]
  body = rest[2]

  alines << ""
  alines << "label #{fn_name}"
  alines << "  push bp"
  alines << "  cp sp bp"

  alines << ""
  alines << "  # 関数の処理本体"
  body.each {|stmt|
    alines << "  # TODO"
  }

  alines << ""
  alines << "  cp bp sp"
  alines << "  pop bp"
  alines << "  ret"

  alines
end

def codegen(tree)
  alines = []

  alines << "  call main"
  alines << "  exit"

  head, *rest = tree
  alines += codegen_func_def(rest)

  alines
end

head, *rest = tree はこの先何度も出てくるイディオムで、 tree (配列)の先頭の要素を head に、 2番目以降の要素を rest に代入するという操作です。

こんな感じの動作になります。

irb(main):001:0> tree = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> head, *rest = tree
=> [1, 2, 3, 4]
irb(main):003:0> head
=> 1
irb(main):004:0> rest
=> [2, 3, 4]

それから、

alines += codegen_func_def(rest)

alines = alines + codegen_func_def(rest)

と同じで、 alinescodegen_func_def(rest) の返り値を繋げたもので alines を上書きする、という動作ですね。

irb(main):023:0> xs = [1, 2]
=> [1, 2]
irb(main):024:0> xs += [3, 4]
=> [1, 2, 3, 4]
irb(main):025:0> xs
=> [1, 2, 3, 4]

要するに codegen_func_def(rest) の結果を alines の末尾に追加しているだけです。 これもこの先何度も使います。


はい、では先に進みましょう。

ルートは stmts にすると決めたので、それに対応します。 今回の vgtコードは

- stmts
  - main() の定義
  - fn_sub() の定義

のように、stmts の直下に関数定義が2つぶら下がる形になっているので、

  • codegen_stmts() というメソッドにルートの stmts を渡して呼び出し、
  • その中で codegen_func_def() を呼び出す

ようにしてみます。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -4,6 +4,8 @@
 
 require 'json'
 
+require './common'
+
 def codegen_func_def(rest)
   alines = []
 
@@ -29,6 +31,22 @@ def codegen_func_def(rest)
   alines
 end
 
+def codegen_stmts(rest)
+  alines = []
+
+  rest.each do |stmt|
+    stmt_head, *stmt_rest = stmt
+    case stmt_head
+    when "func"
+      alines += codegen_func_def(stmt_rest)
+    else
+      raise not_yet_impl("stmt_head", stmt_head)
+    end
+  end
+
+  alines
+end
+
 def codegen(tree)
   alines = []
 
@@ -36,7 +54,8 @@ def codegen(tree)
   alines << "  exit"
 
   head, *rest = tree
-  alines += codegen_func_def(rest)
+  # assert head == "stmts"
+  alines += codegen_stmts(rest)
 
   alines
 end

期待するアセンブリコードに変換されるか試しましょう。

$ ruby vgcg.rb 17_empty_main_sub.vgt.json 
  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体

  cp bp sp
  pop bp
  ret

label fn_sub
  push bp
  cp sp bp

  # 関数の処理本体

  cp bp sp
  pop bp
  ret

いいですね!

ちなみにこの状態で ./run.sh 17_empty_main_sub.vgt.json を実行すると、 main だけ実行されて fn_sub の部分は実行されずに exit します。 fn_sub を呼び出していないので当然そうなりますね、という動作です。 これはこれで OK です(今の段階では)。

main から 別の関数を呼び出す

次は main から fn_sub を呼び出すようにします。

関数を呼び出すための

["call", "{呼び出したい関数名}"]

という構文を新たにでっちあげて main の「関数の処理本体」のところに追加します。

--- a/17_empty_main_sub.vgt.json
+++ b/17_empty_main_sub.vgt.json
@@ -2,7 +2,10 @@
 
 , ["func", "main"
   , []
-  , []
+  , [
+      // 関数の処理本体
+      ["call", "fn_sub"]
+    ]
   ]
 
 , ["func", "fn_sub"

これを変換してこういうアセンブリコードを出力してほしい。

  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体
  call fn_sub # ← これが追加されるだけ

  cp bp sp
  pop bp
  ret

label fn_sub
  push bp
  cp sp bp
  # 関数の処理本体
  cp bp sp
  pop bp
  ret

おや? これは楽勝なのでは?

やってみましょうか。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -20,7 +20,14 @@ def codegen_func_def(rest)
   alines << ""
   alines << "  # 関数の処理本体"
   body.each {|stmt|
-    alines << "  # TODO"
+    stmt_head, *stmt_rest = stmt
+    case stmt_head
+    when "call"
+      fn_name = stmt_rest[0]
+      alines << "  call #{fn_name}"
+    else
+      raise not_yet_impl("stmt_head", stmt_head)
+    end
   }
 
   alines << ""

run.sh で実行してみると……問題なさそうです。 楽勝すぎてちょっと拍子抜けしてしまいましたが ともかく、vgtコードで関数呼び出しが書けるようになりました!

以下は終了時の状態です。

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
pc => 02   ["exit"]
      03 ["label", "main"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["call", 20]
      12   ["cp", "bp", "sp"]
      15   ["pop", "bp"]
      17   ["ret"]
      18 ["label", "fn_sub"]
      20   ["push", "bp"]
      22   ["cp", "sp", "bp"]
      25   ["cp", "bp", "sp"]
      28   ["pop", "bp"]
      30   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 47
         46 12
         47 49
         48 2
sp bp => 49 0

exit