vm2gol v2 製作メモ(21) case文


関数まわりが整備できたので、次は条件分岐(if文)やりましょうか。

えーと、こんな感じで……

["if"
, ["eq", 33, 31]
, [
    ["set_reg_a", 11]
  ]
, [
    ["set_reg_a", 22]
  ]
]

こんな感じで作っていましたが、この後すぐに 「case 文で代用できるから if文はなくても良かったな…」 となって if文のコード生成をなくしてしまいました。

というわけで if文はなかったことにして case文をやります。


こっちが case文の文法です:

// 21_case.vgt.json

["case"

, [ ["eq", 44, 45]    // 条件1
  , ["set_reg_a", 11] // 条件1 が真だった場合に実行
  ]

, [ ["eq", 55, 56]    // 条件2
  , ["set_reg_a", 22] // 条件2 が真だった場合に実行
  ]

  // else節に相当
, [ ["eq", 0, 0]
  , ["set_reg_a", 33]
  ]

]

Lisp の cond ですね。

else 節部分の ["eq", 0, 0] 部分がちょっと不格好ですが、 今の段階でもこうすれば動作としては期待するものになるので、 いったんこれで。 まずは今使えるものでなんとかします (結局最後までこのままなんですが)。

C言語Java の switch文とは違ってフォールスルーはせず、したがって break もなし。

上のコードだと条件1 も条件2 も真にならず else 節が実行されますが、動作確認するときは適当に 条件1 が真になるように書き換えたり、 条件2 が真になるように書き換えたりしてやっていきます (または、そういうパターンを別ファイルで用意してもよいと思います) 。


まずは期待されるアセンブリコードを考えます。 ちょっと長い。

(※ 最終的にはラベルの部分が少し変わります。後述。)

  # 条件1 を評価
  set_reg_a 44
  set_reg_b 45
  compare
  jump_eq when_0 # 条件1 の body にジャンプ

  # 条件2 を評価
  set_reg_a 55
  set_reg_b 56
  compare
  jump_eq when_1 # 条件2 の body にジャンプ

  # 条件3 を評価
  set_reg_a 0
  set_reg_b 0
  compare
  jump_eq when_2 # 条件2 の body にジャンプ

  jump end_case # すべての条件が偽だった場合

# 条件1 の body
label when_0
  set_reg_a 11
  jump end_case

# 条件2 の body
label when_1
  set_reg_a 22
  jump end_case

# 条件3 の body
label when_2
  set_reg_a 33
  jump end_case

label end_case

はい、では入力となる vgtコードを用意して (上のコードを main 関数の中に入れただけ)、

// 21_case.vgt.json

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

      ["case"

      , [ ["eq", 44, 45]    // 条件1
        , ["set_reg_a", 11] // 条件1 が真だった場合に実行
        ]

      , [ ["eq", 55, 56]    // 条件2
        , ["set_reg_a", 22] // 条件2 が真だった場合に実行
        ]

        // else節に相当
      , [ ["eq", 0, 0]
        , ["set_reg_a", 33]
        ]

      ]

    ]
  ]
]

コード生成処理を修正していきましょう。

まずは codegen_case() を追加。 ちょっと分かりにくいかもしれませんが、 それぞれの条件が真だった場合の処理をアセンブリコードの形で then_bodies という配列に溜めておいて、 label_end_case の前でまとめて出力するようにしてみました。

# ※ codegen_func_def() の前に追加

def codegen_case(when_blocks)
  alines = []

  when_idx = -1
  then_bodies = []

  when_blocks.each do |when_block|
    when_idx += 1
    cond, *rest = when_block
    cond_head, *cond_rest = cond
    alines << "  # 条件 #{when_idx}: #{cond.inspect}"

    case cond_head
    when "eq"
      alines << "  set_reg_a #{cond_rest[0]}"
      alines << "  set_reg_b #{cond_rest[1]}"
      alines << "  compare"
      alines << "  jump_eq when_#{when_idx}"

      then_alines = ["label when_#{when_idx}"]
      rest.each {|stmt|
        then_alines << "  " + stmt.join(" ")
      }
      then_alines << "  jump end_case"
      then_bodies << then_alines
    else
      rasie not_yet_impl("cond_head", cond_head)
    end
  end

  # すべての条件が偽だった場合
  alines << "  jump end_case"

  then_bodies.each {|then_alines|
    alines += then_alines
  }

  alines << "label end_case"

  alines
end

codegen_func_def() から呼び出すようにします。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -108,6 +108,8 @@ def codegen_func_def(rest)
     when "return"
       val = stmt_rest[0]
       alines << "  set_reg_a #{val}"
+    when "case"
+      alines += codegen_case(stmt_rest)
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

Vm.num_args_for()jump_eq, jump, compare がなくてエラーになったので追加。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -255,9 +255,9 @@ class Vm
     case operator
     when "cp"
       2
-    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp", "jump_eq", "jump"
       1
-    when "ret", "exit", "add_ab"
+    when "ret", "exit", "add_ab", "compare"
       0
     else
       raise "Invalid operator (#{operator})"

vgtコードを書き換えて、 条件1が真になる場合、条件2が真になる場合、 すべての条件が偽になる場合、などの動作を確認して、 よしよし大丈夫だな……とやっていたのですが、 実はまだダメなんですよ。

よしできたぞーどんどん次に進むぞ―といってこのまま進んでしまうと、 忘れた頃に

_人人人人人人人人人人_
> 複数のcase文 <
 ̄YYYYYYYYY

が登場し、謎の挙動にびっくりしてハマることになります (なりました)。

どういうことかというと、case 文が複数存在した場合でも 使っているラベルが when_0 とか end_case とかなので、 ある case 文から別の case 文のラベルにジャンプしてしまうわけですね。

たとえばこんなコードを動かしてみると、

// 21_cases.vgt.json

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

      ["case"
      , [ ["eq", 11, 12]
        , ["set_reg_a", 22]
        ]
      , [ ["eq", 0, 0]
        , ["set_reg_a", 33]  // ここは実行されない
        ]
      ]

    , ["case"
      , [ ["eq", 44, 45]
        , ["set_reg_a", 55]
        ]
      , [ ["eq", 0, 0]
        , ["set_reg_a", 66]
        ]
      ]

    ]
  ]
]

1つ目の case 文の else から 2つ目の case 文の else 節にジャンプしてしまって、 その後すぐ終了してしまいます。

これはダメですね。対策しないと。


それでどうしたかというと、グローバルなインデックスを安直に用意して、 ラベル名に含めるようにしました。

$label_id を直に使わずに label_id にコピーして使っているのは 入れ子の case文を見越しているため。

あと、 $label_id は case 文以外の while 文などでも共通で使う想定です。 while文の中に case文、みたいなのも登場してくるでしょう。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -6,8 +6,12 @@ require 'json'
 
 require './common'
 
+$label_id = 0
+
 def codegen_case(when_blocks)
   alines = []
+  $label_id += 1
+  label_id = $label_id
 
   when_idx = -1
   then_bodies = []
@@ -16,20 +20,20 @@ def codegen_case(when_blocks)
     when_idx += 1
     cond, *rest = when_block
     cond_head, *cond_rest = cond
-    alines << "  # 条件 #{when_idx}: #{cond.inspect}"
+    alines << "  # 条件 #{label_id}_#{when_idx}: #{cond.inspect}"
 
     case cond_head
     when "eq"
       alines << "  set_reg_a #{cond_rest[0]}"
       alines << "  set_reg_b #{cond_rest[1]}"
       alines << "  compare"
-      alines << "  jump_eq when_#{when_idx}"
+      alines << "  jump_eq when_#{label_id}_#{when_idx}"
 
-      then_alines = ["label when_#{when_idx}"]
+      then_alines = ["label when_#{label_id}_#{when_idx}"]
       rest.each {|stmt|
         then_alines << "  " + stmt.join(" ")
       }
-      then_alines << "  jump end_case"
+      then_alines << "  jump end_case_#{label_id}"
       then_bodies << then_alines
     else
       rasie not_yet_impl("cond_head", cond_head)
@@ -37,13 +41,13 @@ def codegen_case(when_blocks)
   end
 
   # すべての条件が偽だった場合
-  alines << "  jump end_case"
+  alines << "  jump end_case_#{label_id}"
 
   then_bodies.each {|then_alines|
     alines += then_alines
   }
 
-  alines << "label end_case"
+  alines << "label end_case_#{label_id}"
 
   alines
 end

OKです! 次に行きましょう!!

vm2gol v2 製作メモ(20) 値を返却してローカル変数に代入 / return, call_set


関数呼び出しができて、 ローカル変数が使えるようになって、 引数も渡せるようになりました。

あと関数まわりは返り値をどうにかすれば一通り必要なものが揃います。


今回は次の2つのステップに分けます。

  • 値を返すだけ
  • 返された値を(呼び出し元の)ローカル変数にセットする。

ではやっていきましょう。

値を返すだけ

まずはこれをコンパイルします。

// 20_return.vgt.json

["stmts"

, ["func", "main"
  , []
  , [
      ["call", "fn_sub"]
    ]
  ]

, ["func", "fn_sub"
  , []
  , [
      ["return", 12]
    ]
  ]

]

値を返すのは「reg_a に値をセットして ret」すればよいので、 これは簡単ですね。

※参考: (14) 複数の引数を渡す / スタックオーバーフロー対策 / 返り値

["return", {返却する値}]

という構文を追加して、修正します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -52,6 +52,9 @@ def codegen_func_def(rest)
 
       lvar_pos = lvar_names.index(lvar_name) + 1
       alines << "  cp #{val} [bp-#{lvar_pos}]"
+    when "return"
+      val = stmt_rest[0]
+      alines << "  set_reg_a #{val}"
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

簡単ですね。

結果はこうなります。 fn_sub() から戻った直後の状態。

$ ./run.sh 20_return.vgt.json 

(略)

================================
reg_a(12) 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   ["call", 22]
pc => 12   ["add_sp", 0]
      14   ["cp", "bp", "sp"]
      17   ["pop", "bp"]
      19   ["ret"]
      20 ["label", "fn_sub"]
      22   ["push", "bp"]
      24   ["cp", "sp", "bp"]
      27   ["set_reg_a", 12]
      29   ["cp", "bp", "sp"]
      32   ["pop", "bp"]
      34   ["ret"]
---- memory (stack) ----
         39 0
         40 0
         41 0
         42 0
         43 0
         44 0
         45 47
         46 12
sp bp => 47 49
         48 2
         49 0

reg_a に返り値がセットされています。

というわけで、単純な返却はこれで OK。

返り値をローカル変数に代入

次に、返り値をローカル変数に代入します。

["set", "{ローカル変数名}", ["call", "fn_sub"]]

set 構文を改造してこんな感じでローカル変数にセットするとかっこいいかな? というのをまずは考えたのですが、 set の2番めの引数が即値か関数呼び出しかで分岐して……とやっていくと 複雑になりそうな気がして(そうでもない?)、 call_set という専用の構文を新たに追加することにしました。

こうです:

[
  "call_set"
, "{ローカル変数名}"
, [
    "{呼び出す関数名}"
  , {引数1}
  , {引数2}
  , ...
  ]
]

今見ると、内側の括弧をなくして

[
  "call_set"
, "{ローカル変数名}"
, "{呼び出す関数名}"
, {引数1}
, {引数2}
, ...
]

とした方が call の構文に近くて良かったかも…… という気もしますが、うーんまあいいか (適当)。

vgtコードを用意して

// 20_call_set.vgt.json

["stmts"

, ["func", "main"
  , []
  , [
      ["var", "a"]
    , ["call_set", "a", ["fn_sub"]]
    ]
  ]

, ["func", "fn_sub"
  , []
  , [
      ["return", 12]
    ]
  ]

]

call_set 文の処理を追加します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -33,6 +33,17 @@ def codegen_func_def(rest)
       }
       alines << "  call #{fn_name}"
       alines << "  add_sp #{fn_args.size}"
+    when "call_set"
+      lvar_name, fn_temp = stmt_rest
+      fn_name, *fn_args = fn_temp
+      fn_args.reverse.each {|fn_arg|
+        alines << "  push #{fn_arg}"
+      }
+      alines << "  call #{fn_name}"
+      alines << "  add_sp #{fn_args.size}"
+
+      lvar_pos = lvar_names.index(lvar_name) + 1
+      alines << "  cp reg_a [bp-#{lvar_pos}]"
     when "var"
       lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"

call の処理をコピペしてちょこっと直して、 最後に cp reg_a [bp-N] を追加した形ですね。 関数の返り値は reg_a にセットすることにしているので、コピー元は reg_a 固定です。

動かしてみます。

================================
reg_a(12) 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   ["sub_sp", 1]
      12   ["call", 27]
      14   ["add_sp", 0]
      16   ["cp", "reg_a", "[bp-1]"] ... 返り値をローカル変数 a に代入
pc => 19   ["cp", "bp", "sp"]
      22   ["pop", "bp"]
      24   ["ret"]
      25 ["label", "fn_sub"]
      27   ["push", "bp"]
      29   ["cp", "sp", "bp"]
      32   ["set_reg_a", 12]
      34   ["cp", "bp", "sp"]
      37   ["pop", "bp"]
      39   ["ret"]
---- memory (stack) ----
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
         44 47
         45 14
sp    => 46 12 ... ローカル変数 a に代入された
   bp => 47 49
         48 2
         49 0

fn_sub() から main() に戻ってローカル変数 a に返り値を代入した直後です。 いいですね!

これで、関数まわりで必要そうなものがひととおり (といってもほんとに必要最低限ですが)整備できました!

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"] を実行した直後。 いいですね!

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

vm2gol v2 製作メモ(18) ローカル変数の宣言と代入 / var, set


今回はローカル変数の宣言と代入をやります。

アセンブリをやっていたときはループ、条件分岐、 それからサブルーチン〜という順番でやっていました。 それとは違う順番になっていますが、 特に意図したものではなく、次にやることを気分で適当に決めていたためです。 )

vgtコード:

// 18_local_var.vgt.json

["stmts"
, ["func", "main"
  , []
  , [
      ["var", "a"] // ← これ!
    ]
  ]
]

ローカル変数の宣言のために、

["var", "{ローカル変数名}"]

という構文を追加します (func に var と来るとちょっと GO言語っぽいですね)。

期待するアセンブリコード出力:

  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体
  sub_sp 1 # ← これが加わる

  cp bp sp
  pop bp
  ret

これだけなら楽勝ですね。 codegen_func_def()case の分岐を1つ増やすだけ。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -25,6 +25,8 @@ def codegen_func_def(rest)
     when "call"
       fn_name = stmt_rest[0]
       alines << "  call #{fn_name}"
+    when "var"
+      alines << "  sub_sp 1"
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

宣言するだけでは意味がないので、変数に値をセットしてみましょう。

またまた文法を拡張して、こんどは変数に値を代入する

["set", "{ローカル変数名}", {セットしたい値}]

という構文を追加します。

vgtコードの方にも追加。

--- a/18_local_var.vgt.json
+++ b/18_local_var.vgt.json
@@ -3,6 +3,7 @@
   , []
   , [
       ["var", "a"]
+    , ["set", "a", 12]
     ]
   ]
 ]

ちなみに、var a = 12; のように、 宣言と同時に値を代入する構文は用意しませんでした。 varset を使うことで同等のことがすでにできているので、 さっさと次に進もうと考えたためです。

( ただ、ちょっとだけとはいえ煩雑ではあったので、 利便性のために追加しておいても良かったかなという気もします。 そんなに難しくなさそうですし。 )


で、(1個目の)ローカル変数への値のセットは、アセンブリでは cp 12 [bp-1] のように変換されてほしいので、そのように修正します。

参考: (15) ローカル変数 / sub_sp

var と同じように codegen_func_def() の分岐に追加します (ひとまず [bp-1] はハードコーディングで……)。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -27,6 +27,10 @@ def codegen_func_def(rest)
       alines << "  call #{fn_name}"
     when "var"
       alines << "  sub_sp 1"
+    when "set"
+      lvar_name = stmt_rest[0]
+      val = stmt_rest[1]
+      alines << "  cp #{val} [bp-1]"
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

アセンブリコードに変換してみましょう。

$ ruby vgcg.rb 18_local_var.vgt.json 
  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体
  sub_sp 1      # ローカル変数を宣言(格納場所を確保)
  cp 12 [bp-1]  # ローカル変数に値を代入(確保した場所に cp)

  cp bp sp
  pop bp
  ret

よさそうですね。 run.sh で実行すると……

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

えーっとこれは…… 即値は cp 命令のコピー元として使えないようになってますね。 VM を修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -219,6 +219,8 @@ class Vm
   def copy(arg1, arg2)
     src_val =
       case arg1
+      when Integer
+        arg1
       when "reg_a"
         @reg_a
       when "sp"

あらためて実行すると……こんどは大丈夫。

================================
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   ["sub_sp", 1]
      12   ["cp", 12, "[bp-1]"]
pc => 15   ["cp", "bp", "sp"]
      18   ["pop", "bp"]
      20   ["ret"]
---- memory (stack) ----
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
         44 0
         45 0
sp    => 46 12 ... ローカル変数にセットした値
   bp => 47 49
         48 2
         49 0

set 文が動きました!! めでたしめでたし。


めでたく動いたので、ハードコーディングした部分をいい感じにしましょう。 ここをいい感じにすると 2個目以降のローカル変数も使えるようになるはず。

vgtコード:

// 18_local_vars.vgt.json

["stmts"
, ["func", "main"
  , []
  , [
      ["var", "a"]
    , ["set", "a", 12]  // [bp-1] にセットされてほしい

    , ["var", "b"]
    , ["set", "b", 34]  // [bp-2] にセットされてほしい
    ]
  ]
]

上のコメントに書いているように、 1個目の引数 a[bp-1] 、 2個目の引数 b[bp-2] に値がセットされるように変換したい。

変数名から「その変数が何番目に宣言されたか」が分かればよいので、 何かしらのマッピング情報を用意すればいいでしょう。

マッピングってことはハッシュかな、と一瞬考えましたが、 何番目かが分かればいいだけなので、 ハッシュじゃなくて配列を用意して、 変数が出現した(宣言された)順番に追加しとけばいいんじゃないでしょうか。

それでやってみましょう。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -19,6 +19,9 @@ def codegen_func_def(rest)
 
   alines << ""
   alines << "  # 関数の処理本体"
+
+  lvar_names = []
+
   body.each {|stmt|
     stmt_head, *stmt_rest = stmt
     case stmt_head
@@ -26,11 +29,13 @@ def codegen_func_def(rest)
       fn_name = stmt_rest[0]
       alines << "  call #{fn_name}"
     when "var"
+      lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"
     when "set"
       lvar_name = stmt_rest[0]
       val = stmt_rest[1]
-      alines << "  cp #{val} [bp-1]"
+      lvar_pos = lvar_names.index(lvar_name) + 1
+      alines << "  cp #{val} [bp-#{lvar_pos}]"
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end
$ ruby vgcg.rb 18_local_vars.vgt.json 
  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体
  sub_sp 1
  cp 12 [bp-1]
  sub_sp 1
  cp 34 [bp-2]

  cp bp sp
  pop bp
  ret

うまくいったようです!

================================
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   ["sub_sp", 1]
      12   ["cp", 12, "[bp-1]"]
      15   ["sub_sp", 1]
      17   ["cp", 34, "[bp-2]"]
pc => 20   ["cp", "bp", "sp"]
      23   ["pop", "bp"]
      25   ["ret"]
---- memory (stack) ----
         37 0
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
         44 0
sp    => 45 34 ... 2個目のローカル変数
         46 12 ... 1個目のローカル変数
   bp => 47 49
         48 2
         49 0

2個目のローカル変数 b に値をセットした直後の様子。 いい感じです!


これができたら、main から呼び出した先の関数でも 同じようにローカル変数が使えるようになってるんじゃないでしょうか。

調子に乗って試してみましょう。

vgtコード:

// 18_local_vars_call.vgt.json

["stmts"

, ["func", "main"
  , []
  , [
      ["var", "a"]
    , ["set", "a", 12]
    , ["var", "b"]
    , ["set", "b", 34]
    , ["call", "fn_sub"]
    ]
  ]

, ["func", "fn_sub"
  , []
  , [
      ["var", "a"]
    , ["set", "a", 56]
    , ["var", "b"]
    , ["set", "b", 78]
    ]
  ]

]

fn_sub() でローカル変数 b に値がセットされた直後の状態:

================================
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   ["sub_sp", 1]
      12   ["cp", 12, "[bp-1]"]
      15   ["sub_sp", 1]
      17   ["cp", 34, "[bp-2]"]
      20   ["call", 30]
      22   ["cp", "bp", "sp"]
      25   ["pop", "bp"]
      27   ["ret"]
      28 ["label", "fn_sub"]
      30   ["push", "bp"]
      32   ["cp", "sp", "bp"]
      35   ["sub_sp", 1]
      37   ["cp", 56, "[bp-1]"]
      40   ["sub_sp", 1]
      42   ["cp", 78, "[bp-2]"]
pc => 45   ["cp", "bp", "sp"]
      48   ["pop", "bp"]
      50   ["ret"]
---- memory (stack) ----
         33 0
         34 0
         35 0
         36 0
         37 0
         38 0
         39 0
         40 0
sp    => 41 78 ... fn_sub のローカル変数2
         42 56 ... fn_sub のローカル変数1
   bp => 43 47
         44 22
         45 34 ... main のローカル変数2
         46 12 ... main のローカル変数1
         47 49
         48 2
         49 0

おお〜いいですね〜。すんなり〜。 main と同じ変数名( a, b )を使っても ちゃんと別のものとして扱われています。

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

vm2gol v2 製作メモ(16) 簡単なコード生成


条件分岐とループができて、 サブルーチンの呼び出しができて、引数が渡せて、 結果を返せて、ローカル変数が使えるようになった!!! ので、これくらい揃えばもうなんでもできるんじゃね!?!? みたいな気分になりました。 そこで、ここからコード生成に突入しようと思います。

つまり! コンパイラ(の一部)! です!!

( ただし、ここまでで VM は完成という訳ではないので、 VM も必要に応じて修正していきます。 )


まずは空の main 関数呼び出しから始めましょうか。

C言語で書くとこんな感じ(雰囲気で適当に書いてます)。

void main(){
  // 何もしない
}

まじめにパースすると面倒なので、 またまたこんなオレオレフォーマットを使うことにしました。 これをコード生成の入力とします。 JSON なので、 JSON.parse すれば構文木がゲットできます。

// 16_empty_main.vgt.json

["func"
  ,"main" // 関数名
  ,[] // 引数
  ,[] // 関数本体
]

これがアセンブリコードになったときどうなってほしいか?

こうなってほしい!

  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体

  cp bp sp
  pop bp
  ret

このようなアセンブリコードに変換するプログラム(=コード生成器) を作っていきましょう。

入力ファイルの拡張子は、 元のプログラムが C言語相当なつもりなので 最初は .vgc.json としていましたが、 構文木をそのまま書いているようなものなので、 Tree の "t" を取って .vgt.json としました。

この形式のコードのことを「vgtコード」と呼ぶことにします。

コード生成器は vgcg.rb というファイル名にしました。 "cg" は code generator の略。

次のようなコマンドでコード生成を行って アセンブリコードのファイルに出力する想定です。

ruby vgcg.rb foo.vgt.json > foo.vga.txt

さて、まず大枠を作ってみました。 出力するアセンブリコードをハードコーディングしておいて、 最初はこんなとこから始めるといいんじゃないでしょうか?

# vgcg.rb

# aline: assembly line

require 'json'

def codegen(tree)
  alines = []

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

  alines << ""
  alines << "label main"
  alines << "  push bp"
  alines << "  cp sp bp"

  alines << ""
  alines << "  # 関数の処理本体"

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

  alines
end

# vgtコード読み込み
src = File.read(ARGV[0])

# 構文木に変換
tree = JSON.parse(src)

# コード生成(アセンブリコードに変換)
alines = codegen(tree)

# アセンブリコードを出力
alines.each {|aline|
  puts aline
}

大枠としてはこうで、あとは codegen() をそれっぽくしていけばよさそうです。

aline は assembly line の略。 アセンブリコードの1行に対応する文字列ということにします。


codegen() に渡されている tree はこんな内容です (見た目は元の JSON と同じですが、こっちは Ruby の配列です)。

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

入力の内容そのままですね。

ただの配列なので、関数名が欲しければ tree[1] 、 関数の本体部分が欲しければ tree[3] で取り出せます。

こうやって取り出したものでハードコーディングしたところを置き換えていきます。 関数本体はまだ空なので出力としては変化なしですね。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -7,16 +7,22 @@ require 'json'
 def codegen(tree)
   alines = []
 
+  fn_name = tree[1]
+  body = tree[3]
+
   alines << "  call main"
   alines << "  exit"
 
   alines << ""
-  alines << "label main"
+  alines << "label #{fn_name}"
   alines << "  push bp"
   alines << "  cp sp bp"
 
   alines << ""
   alines << "  # 関数の処理本体"
+  body.each {|stmt|
+    alines << "  # TODO"
+  }
 
   alines << ""
   alines << "  cp bp sp"

「関数 main をエントリポイントにする」 と決めたので、一番最初の call main の部分は 置き換えずに決め打ちのままにしています。

動かしてみます。

$ ruby vgcg.rb 16_empty_main.vgt.json 
  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体

  cp bp sp
  pop bp
  ret

特に問題なし。


run.sh を修正しましょう。 アセンブルの前にコード生成のステップを追加します。

--- a/run.sh
+++ b/run.sh
@@ -3,8 +3,10 @@
 set -o errexit
 
 file="$1"
-bname=$(basename $file .vga.txt)
+bname=$(basename $file .vgt.json)
+asmfile=tmp/${bname}.vga.txt
 exefile=tmp/${bname}.vge.yaml
 
-ruby vgasm.rb $file > $exefile
+ruby vgcg.rb $file > $asmfile
+ruby vgasm.rb $asmfile > $exefile
 ruby vgvm.rb $exefile

動かします!

$ ./run.sh 16_empty_main.vgt.json 

(略)

================================
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   ["cp", "bp", "sp"]
      13   ["pop", "bp"]
      15   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 0
         46 0
         47 49
         48 2
sp bp => 49 0

exit

動きました!

今回はここまで。 コード生成の初回なのでまあこんなものでしょう。

vm2gol v2 製作メモ(15) ローカル変数 / sub_sp


今回はローカル変数をやります!


これまでスタックをこんな風に使っていました。

ベースポインタ
戻り先アドレス
引数1
引数2

入れ子の呼び出しがある場合はこう:

ベースポインタ
戻り先アドレス
引数1
引数2
旧ベースポインタ
戻り先アドレス
引数1
引数2

ローカル変数もなんとここに突っ込むそうです。 どう突っ込むかというと、こう:

ローカル変数2
ローカル変数1
ベースポインタ
戻り先アドレス
引数1
引数2
ローカル変数2
ローカル変数1
旧ベースポインタ
戻り先アドレス
引数1
引数2

入れ子なしで見てみます。

      ローカル変数2 (bp-2)
      ローカル変数1 (bp-1)
bp => ベースポインタ
      戻り先アドレス
      引数1 (bp+2)
      引数2 (bp+3)

呼びだされた先のサブルーチンで ローカル変数1を宣言するとスタックポインタが -1 され、 その次に ローカル変数2を宣言するとスタックポインタがさらに -1 されます。

引数1, 2 を使うときは bp+2, bp+3 を見ていましたが、 ローカル変数はベースポインタの上の方を見て、 ローカル変数1, 2 はそれぞれ bp-1, bp-2 の位置を使って読み書きすればよいと。

こういう仕組みになっているそうです! すごいですね! 私はすごいなー巧みな仕組みだなーっていうかこんなんでいいんだ!? と関心して目から鱗でした。 サブルーチン呼び出し、引数渡し、ローカル変数を使うのに スタック1つで用が足りてしまうんですね! この歳になるまで知りませんでしたよ!!!!

ローカル変数1個だけ

関心したところで実際に書いてみます。 一度に少しずつということで、引数と値の返却はなしで、 まずはローカル変数1個だけ。

# 15_local_var.vga.txt

  call sub
  exit

label sub
  push bp
  cp sp bp

  # サブルーチンの処理本体
  set_reg_a 11
  sub_sp 1         # ローカル変数1の宣言(領域確保)
  cp reg_a [bp-1]  # ローカル変数1に値をセット
  cp [bp-1] reg_b  # ローカル変数1の値を参照して reg_b にコピー

  cp bp sp
  pop bp
  ret

まだ sub_sp を実装していないので、動かすとこうなります。

$ ./run.sh 15_local_var.vga.txt 
vgvm.rb:252:in `num_args_for': Invalid operator (sub_sp) (RuntimeError)

sub_sp を追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -160,6 +160,9 @@ class Vm
       when "add_sp"
         set_sp(@sp + @mem.main[@pc + 1])
         @pc += pc_delta
+      when "sub_sp"
+        set_sp(@sp - @mem.main[@pc + 1])
+        @pc += pc_delta
       when "compare"
         compare()
         @pc += pc_delta
@@ -244,7 +247,7 @@ class Vm
     case operator
     when "cp"
       2
-    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp"
       1
     when "ret", "exit", "add_ab"
       0

次はこうなります。

vgvm.rb:229:in `copy': Not yet implemented ("copy src") ("reg_a") (RuntimeError)

copy() を修正。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -219,6 +219,8 @@ class Vm
   def copy(arg1, arg2)
     src_val =
       case arg1
+      when "reg_a"
+        @reg_a
       when "sp"
         @sp
       when "bp"
vgvm.rb:244:in `copy': Not yet implemented ("copy dest") ("[bp-1]") (RuntimeError)

修正します!(ローカル変数へのコピー)

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -240,6 +240,8 @@ class Vm
       @bp = src_val
     when "sp"
       set_sp(src_val)
+    when /^\[bp-(\d+)\]$/
+      @mem.stack[@bp - $1.to_i] = src_val
     else
       raise not_yet_impl("copy dest", arg2)
     end
vgvm.rb:231:in `copy': Not yet implemented ("copy src") ("[bp-1]") (RuntimeError)

修正!!(ローカル変数からのコピー)

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -227,6 +227,8 @@ class Vm
         @bp
       when /^\[bp\+(\d+)\]$/
         @mem.stack[@bp + $1.to_i]
+      when /^\[bp-(\d+)\]$/
+        @mem.stack[@bp - $1.to_i]
       else
         raise not_yet_impl("copy src", arg1)
       end

最後まで動きました!!!

================================
reg_a(11) reg_b(11) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
pc => 02   ["exit"]
      03 ["label", "sub"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["set_reg_a", 11]
      12   ["sub_sp", 1]
      14   ["cp", "reg_a", "[bp-1]"]
      17   ["cp", "[bp-1]", "reg_b"]
      20   ["cp", "bp", "sp"]
      23   ["pop", "bp"]
      25   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 0
         46 11 ... ローカル変数1
         47 49
         48 2
sp bp => 49 0

exit

reg_a → ローカル変数1( [bp-1] ) → reg_b の順番で値がコピーされています。

ローカル変数2個

ローカル変数2個もやってみます。

# 15_local_vars.vga.txt

  call sub
  exit

label sub
  push bp
  cp sp bp

  # サブルーチンの処理本体
  set_reg_a 11
  sub_sp 1         # ローカル変数1の宣言(領域確保)
  cp reg_a [bp-1]  # ローカル変数1に値をセット
  cp [bp-1] reg_b  # ローカル変数1の値を参照して reg_b にコピー

  set_reg_a 12
  sub_sp 1         # ローカル変数2の宣言(領域確保)
  cp reg_a [bp-2]  # ローカル変数2に値をセット
  cp [bp-2] reg_b  # ローカル変数2の値を参照して reg_b にコピー

  cp bp sp
  pop bp
  ret

2個でも大丈夫でした!

================================
reg_a(12) reg_b(12) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 5]
pc => 02   ["exit"]
      03 ["label", "sub"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["set_reg_a", 11]
      12   ["sub_sp", 1]
      14   ["cp", "reg_a", "[bp-1]"]
      17   ["cp", "[bp-1]", "reg_b"]
      20   ["set_reg_a", 12]
      22   ["sub_sp", 1]
      24   ["cp", "reg_a", "[bp-2]"]
      27   ["cp", "[bp-2]", "reg_b"]
      30   ["cp", "bp", "sp"]
      33   ["pop", "bp"]
      35   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 12 ... ローカル変数2
         46 11 ... ローカル変数1
         47 49
         48 2
sp bp => 49 0

exit

3個以上の場合も同じ要領でできそうですね。