vm2gol v2 製作メモ(31) 生存カウント (2) / _cmt



1つのセルの生死が取れるようになったので、 今度は周囲の8つのセルに対して同じように取得できるようにして、 生存カウントを完成させましょう。

前回のものを修正していくので、vgtコードをコピーしておきます。

cp 30_count_alive.vgt.json 31_count_alive_2.vgt.json

まずは、使い回すために先に xl, xr, yt, yb を計算します。 l, r, t, b はそれぞれ left(左), right(右), top(上), bottom(下) の頭文字。

// 31_count_alive_2.vgt.json

, ["func", "count_alive", ["w", "x", "y"]
  , [
      ["var", "count"]
    , ["set", "count", 0]

    , ["var", "xl"]
    , ["var", "xr"]
    , ["var", "yt"]
    , ["var", "yb"]

    , ["set", "xl", ["+", "x", -1]]
    , ["set", "xr", ["+", "x",  1]]
    , ["set", "yt", ["+", "y", -1]]
    , ["set", "yb", ["+", "y",  1]]

    // ...

着目している中心のセルから見て左上のセルの場合は生死を取得するために

    , ["var", "tmp"]
    , ["call_set", "tmp", ["vram_get", "w", "xl", "yt"]]
    , ["set", "count", ["+", "count", "tmp"]]

として、結果を count に足します。

VRAM から取得した結果は 0 か 1 のどちらかなので、 どっちの場合でも単に count に足せばいいでしょう。

あと count_alive() の引数を、注目するセルの座標 (2, 2) にしておきます。

    , ["call", "count_alive", "w", 2, 2]

ここまでの diff:

--- a/31_count_alive_2.vgt.json
+++ b/31_count_alive_2.vgt.json
@@ -29,8 +29,19 @@
       ["var", "count"]
     , ["set", "count", 0]
 
+    , ["var", "xl"]
+    , ["var", "xr"]
+    , ["var", "yt"]
+    , ["var", "yb"]
+
+    , ["set", "xl", ["+", "x", -1]]
+    , ["set", "xr", ["+", "x",  1]]
+    , ["set", "yt", ["+", "y", -1]]
+    , ["set", "yb", ["+", "y",  1]]
+
     , ["var", "tmp"]
-    , ["call_set", "tmp", ["vram_get", "w", "x", "y"]]
+    , ["call_set", "tmp", ["vram_get", "w", "xl", "yt"]]
+    , ["set", "count", ["+", "count", "tmp"]]
     ]
   ]
 
@@ -48,7 +59,7 @@
     , ["set", "y", 0]
 
     , ["call", "vram_set", "w", 1, 1, 1]
-    , ["call", "count_alive", "w", 1, 1]
+    , ["call", "count_alive", "w", 2, 2]
     ]
   ]

で、動かすと

    , ["call_set", "tmp", ["vram_get", "w", "xl", "yt"]]

ここでローカル変数が解決できていなかったので、サッと修正。 このパターンだんだん飽きてきましたね。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -230,6 +230,9 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
       when fn_arg_names.include?(fn_arg)
         fn_arg_addr = to_fn_arg_addr(fn_arg_names, fn_arg)
         alines << "  push #{fn_arg_addr}"
+      when lvar_names.include?(fn_arg)
+        lvar_addr = to_lvar_addr(lvar_names, fn_arg)
+        alines << "  push #{lvar_addr}"
       else
         raise not_yet_impl(fn_arg)
       end

ひとまず左上のセルだけで試してみます。

================================
88: reg_a(1) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      121   ["set_reg_b", 1]
      123   ["add_ab"]
      124   ["cp", "reg_a", "[bp-5]"]
      127   ["sub_sp", 1]
      129   ["push", "[bp-4]"]
      131   ["push", "[bp-2]"]
      133   ["push", "[bp+2]"]
      135   ["call", 41]
      137   ["add_sp", 3]
      139   ["cp", "reg_a", "[bp-6]"]
      142   ["set_reg_a", "[bp-1]"]
      144   ["set_reg_b", "[bp-6]"]
      146   ["add_ab"]
      147   ["cp", "reg_a", "[bp-1]"]
pc => 150   ["cp", "bp", "sp"]
      153   ["pop", "bp"]
      155   ["ret"]
      156 ["label", "main"]
      158   ["push", "bp"]
      160   ["cp", "sp", "bp"]
      163   ["sub_sp", 1]
      165   ["cp", 5, "[bp-1]"]
      168   ["sub_sp", 1]
      170   ["cp", 5, "[bp-2]"]
      173   ["sub_sp", 1]
      175   ["cp", 0, "[bp-3]"]
      178   ["sub_sp", 1]
      180   ["cp", 0, "[bp-4]"]
---- memory (stack) ----
         24 0
         25 6
         26 5
         27 38
         28 137
         29 5
         30 1
         31 1
sp    => 32 1
         33 3
         34 1
         35 3
         36 1
         37 1 ... count
   bp => 38 47
         39 203
         40 5
---- memory (vram) ----
..... .....
.@... .....
..... .....
..... .....
..... .....

ちょっと追いかけるのがしんどいですが、

, ["set", "count", ["+", "count", "tmp"]]

の直後の状態です。 ローカル変数 countbp-1 なので…… 1 になってますね!


前回ステップ数を表示して、早送りするように工夫して、 それだけでも良くはなりましたが、正直なところまだしんどいです。

実行時にステップ実行してメインメモリのダンプ表示(機械語命令の列)と 元の vgtコードを交互ににらめっこして、 「今 pc が指しているのは vgtコードでいうとここだよね?」と確認を繰り返すのがしんどい。 今どこにいるのか見失ってしまうのが辛い……。

それでなくても、デバッグしている自分の脳内に保持すべき情報が多くて 負荷が高い感じです。 関数を呼び出してジャンプするたびに コールスタックを自分で覚えていないといけなくて、 見当識を失ってしまいます。


ではどうするかということで、 ステップ実行していて、たとえば関数呼び出しです。

呼び出し先にジャンプしてしまえば

      36   ["pop", "bp"]
      38   ["ret"]
      39 ["label", "vram_get"]
pc => 41   ["push", "bp"]
      43   ["cp", "sp", "bp"]
      46   ["sub_sp", 1]
      48   ["set_reg_a", "[bp+4]"]

このように表示されていて 「vram_get 関数に入ったんだな」と分かるんですが、 呼び出し元だとこうなっていて、

      129   ["push", "[bp+4]"]
      131   ["push", "[bp+3]"]
      133   ["push", "[bp+2]"]
pc => 135   ["call", 41]
      137   ["add_sp", 3]
      139   ["cp", "reg_a", "[bp-6]"]
      142   ["set_reg_a", "[bp-1]"]

ジャンプする前にどの関数を呼びだそうとしているのか分かりません。 関数呼び出しから戻ったときも、ちょっと気を抜くと 「あれ、今どの関数から戻ったとこなんだっけ……」 となります。

ここに、たとえば

      129   ["push", "[bp+4]"]
      131   ["push", "[bp+3]"]
      133   ["push", "[bp+2]"]
            // call vram_get
pc => 135   ["call", 41]
      137   ["add_sp", 3]
      139   ["cp", "reg_a", "[bp-6]"]
      142   ["set_reg_a", "[bp-1]"]

このように呼び出し先の関数名が表示されていると デバッグ作業の負荷が減らせていい感じになるのではないでしょうか??

というわけで、 _cmt という VM命令を追加します。

オペランドは 1個で、任意の内容のコメント文字列とします。 VMでの実行時には単に何もしない、 つまり、 VM から見ると nop と同じで、 人間がデバッグの目的で見るためだけのものです。

ちょっと特殊な命令なので、頭に _ を付けてみました。


はい、ではまず VM を修正。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -306,6 +306,8 @@ class Vm
         end
 
         @pc += pc_delta
+      when "_cmt"
+        @pc += pc_delta
       else
         raise "Unknown operator (#{op})"
       end
@@ -355,7 +357,7 @@ class Vm
     case operator
     when "cp", "set_vram", "get_vram"
       2
-    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp", "jump_eq", "jump"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp", "jump_eq", "jump", "_cmt"
       1
     when "ret", "exit", "add_ab", "compare", "mult_ab"
       0

コード生成器の call文 , call_set文の部分を修正

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -209,6 +209,7 @@ def codegen_call(lvar_names, stmt_rest)
       raise not_yet_impl(fn_arg)
     end
   }
+  alines << "  _cmt call__#{fn_name}"
   alines << "  call #{fn_name}"
   alines << "  add_sp #{fn_args.size}"
 
@@ -241,6 +242,7 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     end
   }
 
+  alines << "  _cmt call_set__#{fn_name}"
   alines << "  call #{fn_name}"
   alines << "  add_sp #{fn_args.size}"

アセンブリコードをパースするときにスペースで分割しているので、 コメントにはスペースを入れられません。 でもまあ簡易なものだし分かればいいよってことでひとまずこれでよしとして進めます (すぐ後で改良します)。

      185   ["push", 1]
      187   ["push", 1]
      189   ["push", 1]
pc => 191   ["push", "[bp-1]"]
      193   ["_cmt", "call__vram_set"]
      195   ["call", 5]
      197   ["add_sp", 4]
      199   ["push", 1]
      201   ["push", 1]
      203   ["push", "[bp-1]"]
      205   ["_cmt", "call__count_alive"]
      207   ["call", 77]
      209   ["add_sp", 3]
      211   ["cp", "bp", "sp"]
      214   ["pop", "bp"]
      216   ["ret"]

おお、やっぱりいいですね!! こういう、ちょろっと修正するだけで効果が大きい修正ほんと好き。


それから、

, ["set", "count", ["+", "count", "tmp"]]

の直後の状態です。

とさらっと書いていましたが、このときのダンプ表示はこんな感じです。

---- memory (main) ----

      123   ["add_ab"]
      124   ["cp", "reg_a", "[bp-5]"]
      127   ["sub_sp", 1]
      129   ["push", "[bp+4]"]
      131   ["push", "[bp+3]"]
      133   ["push", "[bp+2]"]
      135   ["_cmt", "call_set__vram_get"]
      137   ["call", 41]
      139   ["add_sp", 3]
      141   ["cp", "reg_a", "[bp-6]"]
      144   ["set_reg_a", "[bp-1]"]
      146   ["set_reg_b", "[bp-6]"]
      148   ["add_ab"]
      149   ["cp", "reg_a", "[bp-1]"]
pc => 152   ["cp", "bp", "sp"]
      155   ["pop", "bp"]
      157   ["ret"]
      158 ["label", "main"]
      160   ["push", "bp"]
      162   ["cp", "sp", "bp"]
      165   ["sub_sp", 1]
      167   ["cp", 5, "[bp-1]"]
      170   ["sub_sp", 1]
      172   ["cp", 5, "[bp-2]"]
      175   ["sub_sp", 1]
      177   ["cp", 0, "[bp-3]"]
      180   ["sub_sp", 1]
      182   ["cp", 0, "[bp-4]"]

うーん、これも厳しいですね…… 見落として通りすぎてしまうとまた最初からやり直し、となるので集中力が必要で疲れます。

そこで、 vgtコードでもVMコメントを書けるようにしましょう。

つまり、vgtコードに 「_cmt 文」の構文を追加して

, ["func", "count_alive", ["w", "x", "y"]
  , [
    // ...

    , ["var", "tmp"]
    , ["call_set", "tmp", ["vram_get", "w", "x", "y"]]
    , ["_cmt", "★countに足す"]
    , ["set", "count", ["+", "count", "tmp"]]
    , ["_cmt", "★countに足した直後"]
    ]
  ]

のように書くと、

---- memory (main) ----
      123   ["add_ab"]
      124   ["cp", "reg_a", "[bp-5]"]
      127   ["sub_sp", 1]
      129   ["push", "[bp+4]"]
      131   ["push", "[bp+3]"]
      133   ["push", "[bp+2]"]
      135   ["_cmt", "call_set__vram_get"]
      137   ["call", 41]
      139   ["add_sp", 3]
      141   ["cp", "reg_a", "[bp-6]"]
      ...   ["_cmt", "★countに足す"]
      144   ["set_reg_a", "[bp-1]"]
      146   ["set_reg_b", "[bp-6]"]
      148   ["add_ab"]
      149   ["cp", "reg_a", "[bp-1]"]
      ...   ["_cmt", "★countに足した直後"]
pc => 152   ["cp", "bp", "sp"]
      155   ["pop", "bp"]
      157   ["ret"]
      158 ["label", "main"]
      160   ["push", "bp"]
      162   ["cp", "sp", "bp"]
      165   ["sub_sp", 1]
      167   ["cp", 5, "[bp-1]"]
      170   ["sub_sp", 1]
      172   ["cp", 5, "[bp-2]"]
      175   ["sub_sp", 1]
      177   ["cp", 0, "[bp-3]"]
      180   ["sub_sp", 1]
      182   ["cp", 0, "[bp-4]"]

こんな表示になってめちゃべんりでは!? ということです。

機械語命令の列の中に日本語が出現して謎というか反則くさいですが、 オレオレVMなのでこういうのも好き勝手にやれていいですね! )

やりましょう!!

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -323,6 +323,12 @@ def codegen_return(lvar_names, stmt_rest)
   alines
 end
 
+def codegen_comment(comment)
+  [
+    "  _cmt " + comment.gsub(" ", "~")
+  ]
+end
+
 def codegen_func_def(rest)
   alines = []
 
@@ -360,6 +366,8 @@ def codegen_func_def(rest)
       alines += codegen_case(stmt_rest)
     when "while"
       alines += codegen_while(fn_arg_names, lvar_names, stmt_rest)
+    when "_cmt"
+      alines += codegen_comment(stmt_rest[0])
     else
       raise not_yet_impl("stmt_head", stmt_head)
     end

できました! ちょろい!

VM_cmt 命令はオペランドを 1個だけ受け取るように決めたので、 半角スペースが含まれている場合は適当に置換するようにしてみました。 なんでも良かったのですが、アンダースコアは変数名とかで使われるので、 別にした方がいいかなと思って ~ にしてみました。


せっかくなので、さっき修正した call文 , call_set文でも codegen_comment() を使うようにしましょう。 コメントにスペースを含めていないか気にしなくてもよくなるので、またちょっと便利になりました。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -209,7 +209,7 @@ def codegen_call(lvar_names, stmt_rest)
       raise not_yet_impl(fn_arg)
     end
   }
-  alines << "  _cmt call__#{fn_name}"
+  alines << codegen_comment("call  #{fn_name}")
   alines << "  call #{fn_name}"
   alines << "  add_sp #{fn_args.size}"
 
@@ -242,7 +242,7 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     end
   }
 
-  alines << "  _cmt call_set__#{fn_name}"
+  alines << codegen_comment("call_set  #{fn_name}")
   alines << "  call #{fn_name}"
   alines << "  add_sp #{fn_args.size}"

だいぶ良くなったので、生存カウントに戻りましょう。 左上のセルの生死のカウントができたところなので、 次は上のセルのカウントを追加します。

(2, 1) のセルも生存にして

    , ["call", "vram_set", "w", 2, 1, 1]

2個目のセルのカウントを追加。

    , ["call_set", "tmp", ["vram_get", "w", "x", "yt"]]
    , ["set", "count", ["+", "count", "tmp"]]

    , ["_cmt", "★count_aliveの最後"]

実行してみると、ローカル変数 count の値が 2 になっていることが確認できました!


試しにこうして、

    , ["call", "vram_set", "w", 2, 1, 0]

(2, 1) のセルを死に戻した場合の確認もやってみると、 count == 1 になりました。 良さそう!!


良さそうなので一気に8個に拡張しましょう。 適宜 VMコメントも追加してみます。

--- a/31_count_alive_2.vgt.json
+++ b/31_count_alive_2.vgt.json
@@ -40,14 +40,39 @@
     , ["set", "yb", ["+", "y",  1]]
 
     , ["var", "tmp"]
+
+    , ["_cmt", "左上"]
     , ["call_set", "tmp", ["vram_get", "w", "xl", "yt"]]
-    , ["_cmt", "★countに足す"]
     , ["set", "count", ["+", "count", "tmp"]]
-    , ["_cmt", "★countに足した直後"]
 
+    , ["_cmt", "上"]
     , ["call_set", "tmp", ["vram_get", "w", "x", "yt"]]
     , ["set", "count", ["+", "count", "tmp"]]
 
+    , ["_cmt", "右上"]
+    , ["call_set", "tmp", ["vram_get", "w", "xr", "yt"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
+    , ["_cmt", "左"]
+    , ["call_set", "tmp", ["vram_get", "w", "xl", "y"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
+    , ["_cmt", "右"]
+    , ["call_set", "tmp", ["vram_get", "w", "xr", "y"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
+    , ["_cmt", "左下"]
+    , ["call_set", "tmp", ["vram_get", "w", "xl", "yb"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
+    , ["_cmt", "下"]
+    , ["call_set", "tmp", ["vram_get", "w", "x", "yb"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
+    , ["_cmt", "右下"]
+    , ["call_set", "tmp", ["vram_get", "w", "xr", "yb"]]
+    , ["set", "count", ["+", "count", "tmp"]]
+
     , ["_cmt", "★count_aliveの最後"]
     ]
   ]
@@ -67,6 +92,14 @@
 
     , ["call", "vram_set", "w", 1, 1, 1]
     , ["call", "vram_set", "w", 2, 1, 1]
+    , ["call", "vram_set", "w", 3, 1, 1]
+
+    , ["call", "vram_set", "w", 1, 2, 1]
+    , ["call", "vram_set", "w", 3, 2, 1]
+
+    , ["call", "vram_set", "w", 1, 3, 1]
+    , ["call", "vram_set", "w", 2, 3, 1]
+    , ["call", "vram_set", "w", 3, 3, 1]
 
     , ["call", "count_alive", "w", 2, 2]
     ]

実行!

================================
449: reg_a(8) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      300   ["add_ab"]
      301   ["cp", "reg_a", "[bp-1]"]
      304   ["_cmt", "右下"]
      306   ["push", "[bp-5]"]
      308   ["push", "[bp-3]"]
      310   ["push", "[bp+2]"]
      312   ["_cmt", "call_set~~vram_get"]
      314   ["call", 41]
      316   ["add_sp", 3]
      318   ["cp", "reg_a", "[bp-6]"]
      321   ["set_reg_a", "[bp-1]"]
      323   ["set_reg_b", "[bp-6]"]
      325   ["add_ab"]
      326   ["cp", "reg_a", "[bp-1]"]
pc => 329   ["_cmt", "★count_aliveの最後"]
      331   ["cp", "bp", "sp"]
      334   ["pop", "bp"]
      336   ["ret"]
      337 ["label", "main"]
      339   ["push", "bp"]
      341   ["cp", "sp", "bp"]
      344   ["sub_sp", 1]
      346   ["cp", 5, "[bp-1]"]
      349   ["sub_sp", 1]
      351   ["cp", 5, "[bp-2]"]
      354   ["sub_sp", 1]
      356   ["cp", 0, "[bp-3]"]
      359   ["sub_sp", 1]
---- memory (stack) ----
         24 0
         25 18
         26 15
         27 38
         28 316
         29 5
         30 3
         31 3
sp    => 32 1
         33 3
         34 1
         35 3
         36 1
         37 8 ... count
   bp => 38 47
         39 486
         40 5
---- memory (vram) ----
..... .....
.@@@. .....
.@.@. .....
.@@@. .....
..... .....

count が 8 になりました!!!


念のため、いくつかのセルを死に戻した場合も確認しておきましょう。

================================
403: reg_a(6) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      300   ["add_ab"]
      301   ["cp", "reg_a", "[bp-1]"]
      304   ["_cmt", "右下"]
      306   ["push", "[bp-5]"]
      308   ["push", "[bp-3]"]
      310   ["push", "[bp+2]"]
      312   ["_cmt", "call_set~~vram_get"]
      314   ["call", 41]
      316   ["add_sp", 3]
      318   ["cp", "reg_a", "[bp-6]"]
      321   ["set_reg_a", "[bp-1]"]
      323   ["set_reg_b", "[bp-6]"]
      325   ["add_ab"]
      326   ["cp", "reg_a", "[bp-1]"]
pc => 329   ["_cmt", "★count_aliveの最後"]
      331   ["cp", "bp", "sp"]
      334   ["pop", "bp"]
      336   ["ret"]
      337 ["label", "main"]
      339   ["push", "bp"]
      341   ["cp", "sp", "bp"]
      344   ["sub_sp", 1]
      346   ["cp", 5, "[bp-1]"]
      349   ["sub_sp", 1]
      351   ["cp", 5, "[bp-2]"]
      354   ["sub_sp", 1]
      356   ["cp", 0, "[bp-3]"]
      359   ["sub_sp", 1]
---- memory (stack) ----
         24 0
         25 18
         26 15
         27 38
         28 316
         29 5
         30 3
         31 3
sp    => 32 1
         33 3
         34 1
         35 3
         36 1
         37 6 ... count
   bp => 38 47
         39 458
         40 5
---- memory (vram) ----
..... .....
.@@.. .....
.@.@. .....
..@@. .....
..... .....

ちゃんとカウントできているようです!

何かこう、「すごい……ほんとにちゃんと動いてる……」 みたいな不思議な感動があります!!

いやー楽しい。 めんどくさいことやって苦労してますが、報われる感じがしますね。

追記 2021-10-03

ダンプ表示時にVMコメントに色を付けておくと見やすいのでおすすめです。 _cmt を追加した時にあわせてやっておけばよかったのですが、忘れていました。

step 38 で修正してますのでそちらを参照してください。

f:id:sonota88:20191005110106p:plain