vm2gol v2 製作メモ(22) 組み込みの足し算


分岐ができたので次はループをやりたいですね。 簡単なところで、数を1ずつ増やしていくカウンタとか。

  ["var", "a"]
, ["set", "a", 0]
, ["while"
  , ["eq", 0, 0]  // 条件: 必ず真になるようにして無限ループさせる
  , [
      // ループの本体
      ["set", "a", {a+1}]  // 変数 a をインクリメント
    ]
  ]

これをやりたいんですが、足し算がまだできないので、 先に組み込みの足し算機能を作ります。

足し算

こんな構文を用意して……

["+", 1, 2]

と思ったのですが、実際に使うときは式単体ではなく 式を評価した結果をローカル変数に入れて使うでしょうから、 set文を改造して、

["set", {ローカル変数}, ["+", 1, 2]]

という構文で書けるようにします。

vgtコードです。

// 22_set_add.vgt.json

["stmts"
, ["func", "main"
  , []
  , [
      ["var", "a"]
    , ["set", "a", ["+", 1, 2]]
    ]
  ]
]

こういうアセンブリコードになってほしい(set文の部分のみ):

  set_reg_a 1
  set_reg_b 2
  add_ab           # 結果が reg_a に入る
  cp reg_a [bp-1]  # ローカル変数にセット

修正しましょう。


codegen_func_def() の case 文が長くなってきたので、 まずは set文の部分をメソッドに抽出します。 動作は変わらず、単純なリファクタリングです。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -52,6 +52,27 @@ def codegen_case(when_blocks)
   alines
 end
 
+def codegen_set(fn_arg_names, lvar_names, rest)
+  alines = []
+  lvar_name = rest[0]
+
+  src_val =
+    case
+    when rest[1].is_a?(Integer)
+      rest[1]
+    when fn_arg_names.include?(rest[1])
+      fn_arg_pos = fn_arg_names.index(rest[1]) + 2
+      "[bp+#{fn_arg_pos}]"
+    else
+      raise not_yet_impl("set src_val", rest)
+    end
+
+  lvar_pos = lvar_names.index(lvar_name) + 1
+  alines << "  cp #{src_val} [bp-#{lvar_pos}]"
+
+  alines
+end
+
 def codegen_func_def(rest)
   alines = []
 
@@ -94,21 +115,7 @@ def codegen_func_def(rest)
       lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"
     when "set"
-      lvar_name = stmt_rest[0]
-
-      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}]"
+      alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
     when "return"
       val = stmt_rest[0]
       alines << "  set_reg_a #{val}"

はい、では足し算とローカル変数へのセットをやりましょう。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -52,6 +52,25 @@ def codegen_case(when_blocks)
   alines
 end
 
+def codegen_exp(exp)
+  alines = []
+  operator, *args = exp
+
+  left = args[0]
+  right = args[1]
+
+  case operator
+  when "+"
+    alines << "  set_reg_a #{left}"
+    alines << "  set_reg_b #{right}"
+    alines << "  add_ab"
+  else
+    raise not_yet_impl("operator", operator)
+  end
+
+  alines
+end
+
 def codegen_set(fn_arg_names, lvar_names, rest)
   alines = []
   lvar_name = rest[0]
@@ -60,6 +79,10 @@ def codegen_set(fn_arg_names, lvar_names, rest)
     case
     when rest[1].is_a?(Integer)
       rest[1]
+    when rest[1].is_a?(Array)
+      exp = rest[1]
+      alines += codegen_exp(exp)
+      "reg_a"
     when fn_arg_names.include?(rest[1])
       fn_arg_pos = fn_arg_names.index(rest[1]) + 2
       "[bp+#{fn_arg_pos}]"

exp は expression(式)の略です。

set文の2つ目の引数が配列だった場合の分岐を追加して、 codegen_exp() では ["+", 1, 2] を評価した結果を reg_a に入れて codegen_set() に戻る、という作りです。

この diff だと codegen_set() がどうなったか ちょっと分かりにくいかもしれないので全部貼っておきます。

def codegen_set(fn_arg_names, lvar_names, rest)
  alines = []
  lvar_name = rest[0]

  src_val =
    case
    when rest[1].is_a?(Integer)
      rest[1]
    when rest[1].is_a?(Array)
      exp = rest[1]
      alines += codegen_exp(exp)
      "reg_a"
    when fn_arg_names.include?(rest[1])
      fn_arg_pos = fn_arg_names.index(rest[1]) + 2
      "[bp+#{fn_arg_pos}]"
    else
      raise not_yet_impl("set src_val", rest)
    end

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

  alines
end

run.sh で実行して、足し算の結果がローカル変数 a にセットされた直後の状態:

================================
reg_a(3) reg_b(2) 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   ["set_reg_a", 1]
      14   ["set_reg_b", 2]
      16   ["add_ab"]
      17   ["cp", "reg_a", "[bp-1]"]
pc => 20   ["cp", "bp", "sp"]
      23   ["pop", "bp"]
      25   ["ret"]
---- memory (stack) ----
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
         44 0
         45 0
sp    => 46 3  ... ローカル変数 a
   bp => 47 49
         48 2
         49 0

よしよし。

ローカル変数のインクリメント

即値どうしの足し算ができるようになりましたが、 カウンタで必要なのはローカル変数のインクリメントです。 ローカル変数と即値の足し算にも対応させましょう。

vgtコード:

// 22_lvar_increment.vgt.json

["stmts"
, ["func", "main"
  , []
  , [
      ["var", "a"]
    , ["set", "a", 12]
    , ["set", "a", ["+", "a", 1]]
    ]
  ]
]

ちなみに現時点では次のようになってしまってダメです。 reg_a"a" という文字列が入ってしまい、 文字列と数を足し算しようとしてエラーになります。

================================
reg_a("a") reg_b(1) 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   ["set_reg_a", "a"]
      17   ["set_reg_b", 1]
pc => 19   ["add_ab"]
      20   ["cp", "reg_a", "[bp-1]"]
      23   ["cp", "bp", "sp"]
      26   ["pop", "bp"]
      28   ["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

vgvm.rb:303:in `+': no implicit conversion of Fixnum into String (TypeError)
        from vgvm.rb:303:in `add_ab'
        from vgvm.rb:155:in `block in start'
        from vgvm.rb:126:in `loop'
        from vgvm.rb:126:in `start'
        from vgvm.rb:330:in `<main>'

修正します。 ローカル変数を解決しないといけないですね。 これは前にもやったので同じようにやればよいでしょう。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -52,11 +52,26 @@ def codegen_case(when_blocks)
   alines
 end
 
-def codegen_exp(exp)
+def codegen_exp(lvar_names, exp)
   alines = []
   operator, *args = exp
 
-  left = args[0]
+  left =
+    case args[0]
+    when Integer
+      args[0]
+    when String
+      case
+      when lvar_names.include?(args[0])
+        lvar_pos = lvar_names.index(args[0]) + 1
+        "[bp-#{lvar_pos}]"
+      else
+        raise not_yet_impl("left", args[0])
+      end
+    else
+      raise not_yet_impl("left", args[0])
+    end
+
   right = args[1]
 
   case operator
@@ -81,7 +96,7 @@ def codegen_set(fn_arg_names, lvar_names, rest)
       rest[1]
     when rest[1].is_a?(Array)
       exp = rest[1]
-      alines += codegen_exp(exp)
+      alines += codegen_exp(lvar_names, exp)
       "reg_a"
     when fn_arg_names.include?(rest[1])
       fn_arg_pos = fn_arg_names.index(rest[1]) + 2

動かしてみると……

================================
reg_a("[bp-1]") reg_b(1) 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   ["set_reg_a", "[bp-1]"]
      17   ["set_reg_b", 1]
pc => 19   ["add_ab"]
      20   ["cp", "reg_a", "[bp-1]"]
      23   ["cp", "bp", "sp"]
      26   ["pop", "bp"]
      28   ["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

vgvm.rb:303:in `+': no implicit conversion of Fixnum into String (TypeError)
        from vgvm.rb:303:in `add_ab'
        from vgvm.rb:155:in `block in start'
        from vgvm.rb:126:in `loop'
        from vgvm.rb:126:in `start'
        from vgvm.rb:330:in `<main>'

ダメですね。なんでしょう? えーと……あ、今度は reg_a"[bp-1]" という文字列が入っちゃってます。

コード生成時にローカル変数が解決されて [bp-1] になり、 そのまま機械語コードになっているところまではOK。 ということはこれはコード生成器やアセンブラではなく VM の問題です。

set_reg_a が即値にしか対応していなかったのが原因です。 オペランド[bp-1] となっていたら、 [bp-1] が指しているスタックの実アドレスを reg_a にセットしてほしい。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -134,8 +134,8 @@ class Vm
         $stderr.puts "exit"
         exit
       when "set_reg_a"
-        n = @mem.main[@pc + 1]
-        @reg_a = n
+        val = @mem.main[@pc + 1]
+        set_reg_a(val)
         @pc += pc_delta
       when "set_reg_b"
         n = @mem.main[@pc + 1]
@@ -307,6 +307,19 @@ class Vm
     @reg_a = @reg_a + @reg_c
   end
 
+  def set_reg_a(val)
+    @reg_a =
+      case val
+      when Integer
+        val
+      when /^\[bp-(\d+)\]$/
+        stack_addr = @bp - $1.to_i
+        @mem.stack[stack_addr]
+      else
+        raise not_yet_impl("val", val)
+      end
+  end
+
   def compare
     @zf = (@reg_a == @reg_b) ? 1 : 0
   end

これで動くようになりました。

================================
reg_a(13) reg_b(1) 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   ["set_reg_a", "[bp-1]"]
      17   ["set_reg_b", 1]
      19   ["add_ab"]
      20   ["cp", "reg_a", "[bp-1]"]
pc => 23   ["cp", "bp", "sp"]
      26   ["pop", "bp"]
      28   ["ret"]
---- memory (stack) ----
         38 0
         39 0
         40 0
         41 0
         42 0
         43 0
         44 0
         45 0
sp    => 46 13
   bp => 47 49
         48 2
         49 0

12 + 1 が bp-1 の位置(ローカル変数 a)にセットされてます!


カウンタを作るための準備ができたので次は while を…… といきたいところですが、長くなってきたので次に回します!

今回はここまで!!