vm2gol v2 製作メモ(30) 生存カウント / VRAM から値を取得 / ステップ数を表示



$stdin.gets を無効化

前回二重ループですべてのセルを生存に変えていくというのをやりましたが、 ループがぐるぐる回るため何度も同じ処理を繰り返すようになり、 Enter キーを押しっぱなしにしないといけなくなってきました。

これはかったるいです。なんとかしましょう。

(かったるいですが、たったこれだけのことをやるのでも こんなにいっぱい命令を処理しているんだなあ……と実感できるので これはこれでよい体験だと思います )

Enter キーで 1ステップごとに進めるのをやめて、 勝手に動くようにしてみましょうか。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -285,7 +285,7 @@ class Vm
       end
 
       dump_v2()
-      $stdin.gets
+      # $stdin.gets
     end
   end

完全に削除せずにコメントアウトにしておきました。

これで前回の 29_loop_xy.vgt.json を動かすと一瞬ですべてのセルが生存になり、 おおっ、すごい! となるのですが、一方で、一瞬でババッと動いて終わってしまうので、少し寂しさがあります。 というか速すぎて何が起こってるか全然分からない……。

sleep を入れてみましょうか。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -286,6 +286,7 @@ class Vm
 
       dump_v2()
       # $stdin.gets
+      sleep 0.01
     end
   end

うーむ。まあこれで進めてみましょう。

vram_get()

ライフゲームの話に戻ります。

次にどこから手を付けるかちょっと悩むところですが、 あるセルの周囲にある 8個のセルを調べ、 何個が生きているかを調べる関数 count_alive() を作りましょうか。

Ruby で書いたものでいうとここです:

      xl = (x == 0     ) ? $w - 1 : x - 1
      xr = (x == $w - 1) ? 0      : x + 1
      yt = (y == 0     ) ? $h - 1 : y - 1
      yb = (y == $h - 1) ? 0      : y + 1

      n = 0
      n += $grid[yt][xl]
      n += $grid[y ][xl]
      n += $grid[yb][xl]
      n += $grid[yt][x ]
      n += $grid[yb][x ]
      n += $grid[yt][xr]
      n += $grid[y ][xr]
      n += $grid[yb][xr]

まずは端っこの方は無視して、盤面の中心でカウントが正しく動くことを確認しようと思います。 中心のセル(座標でいうと (2, 2) )に着目しているとして、たとえば

.....
.@...
.....
.....
.....

という状態であれば生存数 1、

.....
.@@@.
.@.@.
.@@@.
.....

だったら 8 、が得られることを目指します。


そのためには VRAM から値を読み取る vram_get() が必要で……まだなかったですね。今回はこれを作ります。

  • vram_set() をコピーしてちょっと修正して vram_get() を用意。
  • count_alive() 関数を作って main() から呼び出す。 まずは適当なセルの値を取得して、ローカル変数 tmp にセットするだけ。
// 30_count_alive.vgt.json

["stmts"

, ["func", "vram_set", ["w", "x", "y", "val"]
  , [
      ["var", "yw"]
    , ["set", "yw", ["*", "y", "w"]]

    , ["var", "vi"] // vram index
    , ["set", "vi", ["+", "yw", "x"]]

    , ["set", "vram[vi]", "val"]
    ]
  ]

, ["func", "vram_get", ["w", "x", "y"]
  , [
      ["var", "yw"]
    , ["set", "yw", ["*", "y", "w"]]

    , ["var", "vi"] // vram index
    , ["set", "vi", ["+", "yw", "x"]]

    , ["return", "vram[vi]"]
    ]
  ]

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

    , ["var", "tmp"]
    , ["call_set", "tmp", ["vram_get", "w", "x", "y"]]
    ]
  ]

, ["func", "main", []
  , [
      ["var", "w"] // 盤面の幅
    , ["set", "w", 5]
    , ["var", "h"] // 盤面の高さ
    , ["set", "h", 5]

    , ["var", "x"]
    , ["set", "x", 0]

    , ["var", "y"]
    , ["set", "y", 0]

    , ["call", "count_alive", "w", 1, 1]
    ]
  ]

]

実行。

================================
reg_a(6) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      58   ["set_reg_a", "[bp-1]"]
      60   ["set_reg_b", "[bp+3]"]
      62   ["add_ab"]
      63   ["cp", "reg_a", "[bp-2]"]
      66   ["set_reg_a", "vram[vi]"]
      68   ["cp", "bp", "sp"]
      71   ["pop", "bp"]
      73   ["ret"]
      74 ["label", "count_alive"]
      76   ["push", "bp"]
      78   ["cp", "sp", "bp"]
      81   ["sub_sp", 1]
      83   ["cp", 0, "[bp-1]"]
      86   ["sub_sp", 1]
pc => 88   ["push", "y"]
      90   ["push", "x"]
      92   ["push", "w"]
      94   ["call", 41]
      96   ["add_sp", 3]
      98   ["cp", "reg_a", "[bp-2]"]
      101   ["cp", "bp", "sp"]
      104   ["pop", "bp"]
      106   ["ret"]
      107 ["label", "main"]
      109   ["push", "bp"]
      111   ["cp", "sp", "bp"]
      114   ["sub_sp", 1]
      116   ["cp", 5, "[bp-1]"]
---- memory (stack) ----
         28 0
         29 0
         30 0
         31 0
         32 0
         33 0
         34 0
         35 6
sp    => 36 5
         37 0
   bp => 38 47
         39 154
         40 5
         41 1
         42 1
         43 0
         44 0
---- memory (vram) ----
..... .....
.@... .....
..... .....
..... .....
..... .....
vgvm.rb:227:in `block in start': Not yet implemented ("push") ("y") (RuntimeError)
    from vgvm.rb:150:in `loop'
    from vgvm.rb:150:in `start'
    from vgvm.rb:445:in `<main>'

↓このようになっていて、関数の引数が解決されずにそのまま VM に渡っています。またこのパターンですね。

pc => 88   ["push", "y"]
      90   ["push", "x"]
      92   ["push", "w"]

修正します。 codegen_call_set() の引数に fn_arg_names を追加して 参照を解決してあげます。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -215,14 +215,29 @@ def codegen_call(lvar_names, stmt_rest)
   alines
 end
 
-def codegen_call_set(lvar_names, stmt_rest)
+def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
   alines = []
 
   lvar_name, fn_temp = stmt_rest
   fn_name, *fn_args = fn_temp
+
   fn_args.reverse.each {|fn_arg|
-    alines << "  push #{fn_arg}"
+    case fn_arg
+    when Integer
+      alines << "  push #{fn_arg}"
+    when String
+      case
+      when fn_arg_names.include?(fn_arg)
+        fn_arg_addr = to_fn_arg_addr(fn_arg_names, fn_arg)
+        alines << "  push #{fn_arg_addr}"
+      else
+        raise not_yet_impl(fn_arg)
+      end
+    else
+      raise not_yet_impl(fn_arg)
+    end
   }
+
   alines << "  call #{fn_name}"
   alines << "  add_sp #{fn_args.size}"
 
@@ -297,7 +312,7 @@ def codegen_func_def(rest)
     when "call"
       alines += codegen_call(lvar_names, stmt_rest)
     when "call_set"
-      alines += codegen_call_set(lvar_names, stmt_rest)
+      alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     when "var"
       lvar_names << stmt_rest[0]
       alines << "  sub_sp 1"

何度もやってきたので慣れたものですね。

実行。

$ ./run.sh 30_count_alive.vgt.json

(略)

      74 ["label", "count_alive"]
      76   ["push", "bp"]
      78   ["cp", "sp", "bp"]
      81   ["sub_sp", 1]
      83   ["cp", 0, "[bp-1]"]
      86   ["sub_sp", 1]
pc => 88   ["push", "[bp+4]"]

(略)

vgvm.rb:227:in `block in start': Not yet implemented ("push") ("[bp+4]") (RuntimeError)
    from vgvm.rb:150:in `loop'
    from vgvm.rb:150:in `start'
    from vgvm.rb:445:in `<main>'

今度は VM です。 関数の引数の push ってまだやってなかったんですね。

修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -223,6 +223,9 @@ class Vm
             when /^\[bp\-(\d+)\]$/
               stack_addr = @bp - $1.to_i
               @mem.stack[stack_addr]
+            when /^\[bp\+(\d+)\]$/
+              stack_addr = @bp + $1.to_i
+              @mem.stack[stack_addr]
             else
               raise not_yet_impl("push", arg)
             end

実行。

$ ./run.sh 30_count_alive.vgt.json 

(略)

      39 ["label", "vram_get"]
      41   ["push", "bp"]
      43   ["cp", "sp", "bp"]
      46   ["sub_sp", 1]
      48   ["set_reg_a", "[bp+4]"]
      50   ["set_reg_b", "[bp+2]"]
      52   ["mult_ab"]
      53   ["cp", "reg_a", "[bp-1]"]
      56   ["sub_sp", 1]
      58   ["set_reg_a", "[bp-1]"]
      60   ["set_reg_b", "[bp+3]"]
      62   ["add_ab"]
      63   ["cp", "reg_a", "[bp-2]"]
pc => 66   ["set_reg_a", "vram[vi]"]

(略)

vgvm.rb:408:in `set_reg_a': Not yet implemented ("val") ("vram[vi]") (RuntimeError)
    from vgvm.rb:162:in `block in start'
    from vgvm.rb:150:in `loop'
    from vgvm.rb:150:in `start'
    from vgvm.rb:448:in `<main>'

えーっと…… vi というのはローカル変数の名前なので、 機械語コードにこれが登場してるのはおかしい、というやつ。

, ["return", "vram[vi]"]

ここです。 return 文のとこを修正します。

ちょっと長めになったので codegen_return() にメソッド抽出しました。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -289,6 +289,35 @@ def codegen_set(fn_arg_names, lvar_names, rest)
   alines
 end
 
+def codegen_return(lvar_names, stmt_rest)
+  alines = []
+
+  retval = stmt_rest[0]
+
+  case retval
+  when Integer
+    alines << "  set_reg_a #{retval}"
+  when String
+    case retval
+    when /^vram\[([a-z0-9_]+)\]$/
+      var_name = $1
+      case
+      when lvar_names.include?(var_name)
+        lvar_addr = to_lvar_addr(lvar_names, var_name)
+        alines << "  get_vram #{lvar_addr} reg_a"
+      else
+        raise not_yet_impl("retval", retval)
+      end
+    else
+      raise not_yet_impl("retval", retval)
+    end
+  else
+    raise not_yet_impl("retval", retval)
+  end
+
+  alines
+end
+
 def codegen_func_def(rest)
   alines = []
 
@@ -321,8 +350,7 @@ def codegen_func_def(rest)
     when "eq"
       alines += codegen_exp(fn_arg_names, lvar_names, stmt)
     when "return"
-      val = stmt_rest[0]
-      alines << "  set_reg_a #{val}"
+      alines += codegen_return(lvar_names, stmt_rest)
     when "case"
       alines += codegen_case(stmt_rest)
     when "while"

実行。

$ ./run.sh 30_count_alive.vgt.json 

(略)

      39 ["label", "vram_get"]
      41   ["push", "bp"]
      43   ["cp", "sp", "bp"]
      46   ["sub_sp", 1]
      48   ["set_reg_a", "[bp+4]"]
      50   ["set_reg_b", "[bp+2]"]
      52   ["mult_ab"]
      53   ["cp", "reg_a", "[bp-1]"]
      56   ["sub_sp", 1]
      58   ["set_reg_a", "[bp-1]"]
      60   ["set_reg_b", "[bp+3]"]
      62   ["add_ab"]
      63   ["cp", "reg_a", "[bp-2]"]
pc => 66   ["get_vram", "[bp-2]", "reg_a"]
      69   ["cp", "bp", "sp"]
      72   ["pop", "bp"]
      74   ["ret"]

(略)

vgvm.rb:279:in `[]': no implicit conversion of String into Integer (TypeError)
    from vgvm.rb:279:in `block in start'
    from vgvm.rb:150:in `loop'
    from vgvm.rb:150:in `start'
    from vgvm.rb:448:in `<main>'

さっき修正した return の部分ですが、今度は VMget_vram の方を修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -276,7 +276,23 @@ class Vm
         arg1 = @mem.main[@pc + 1]
         arg2 = @mem.main[@pc + 2]
 
-        val = @mem.vram[arg1]
+        vram_addr =
+          case arg1
+          when Integer
+            arg1
+          when String
+            case arg1
+            when /^\[bp\-(\d+)\]$/
+              stack_addr = $1.to_i
+              @mem.stack[stack_addr]
+            else
+              raise not_yet_impl("arg1", arg1)
+            end
+          else
+            raise not_yet_impl("arg1", arg1)
+          end
+
+        val = @mem.vram[vram_addr]
 
         case arg2
         when "reg_a"

これでエラーが出なくなりました!

出なくなりましたが、ババッと動いて終了してしまうので やっぱり何が起こってるのかよく分からん…… というか何してたんだっけ…… あ、生存カウントでした。

コメントアウトしたばかりですが $stdin.gets をコメントインして結果確認しましょうか。うーむ。

================================
reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      69   ["cp", "bp", "sp"]
      72   ["pop", "bp"]
      74   ["ret"]
      75 ["label", "count_alive"]
      77   ["push", "bp"]
      79   ["cp", "sp", "bp"]
      82   ["sub_sp", 1]
      84   ["cp", 0, "[bp-1]"]
      87   ["sub_sp", 1]
      89   ["push", "[bp+4]"]
      91   ["push", "[bp+3]"]
      93   ["push", "[bp+2]"]
      95   ["call", 41]
pc => 97   ["add_sp", 3]  # vram_get() から戻った直後
      99   ["cp", "reg_a", "[bp-2]"]
      102   ["cp", "bp", "sp"]
      105   ["pop", "bp"]
      107   ["ret"]
      108 ["label", "main"]

(略)

---- memory (vram) ----
..... .....
..... .....
..... .....
..... .....
..... .....

実行途中の様子。 vram_get() から戻ってきた直後です。

VRAM の (1, 1) のセルが死亡状態、 reg_a が 0 になっており、期待する結果になっているようです。 見た目の変化がないのでそこはかとなく不安ではありますが。


今度は (1, 1) のセルを生存状態にして、同じ箇所で reg_a が 1 になることを確認。

// 30_count_alive.vgt.json

+    , ["call", "vram_set", "w", 1, 1, 1] // この行を追加。 (1, 1) のセルを生存状態にする。
     , ["call", "count_alive", "w", 1, 1]

実行します。

================================
reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      69   ["cp", "bp", "sp"]
      72   ["pop", "bp"]
      74   ["ret"]
      75 ["label", "count_alive"]
      77   ["push", "bp"]
      79   ["cp", "sp", "bp"]
      82   ["sub_sp", 1]
      84   ["cp", 0, "[bp-1]"]
      87   ["sub_sp", 1]
      89   ["push", "[bp+4]"]
      91   ["push", "[bp+3]"]
      93   ["push", "[bp+2]"]
      95   ["call", 41]               # vram_get() の呼び出し
pc => 97   ["add_sp", 3]              # vram_get() から戻った直後
      99   ["cp", "reg_a", "[bp-2]"]
      102   ["cp", "bp", "sp"]
      105   ["pop", "bp"]
      107   ["ret"]
      108 ["label", "main"]
      110   ["push", "bp"]
      112   ["cp", "sp", "bp"]
      115   ["sub_sp", 1]
      117   ["cp", 5, "[bp-1]"]
      120   ["sub_sp", 1]
      122   ["cp", 5, "[bp-2]"]
      125   ["sub_sp", 1]
      127   ["cp", 0, "[bp-3]"]
---- memory (stack) ----
         25 0
         26 0
         27 0
         28 0
         29 6
         30 5
         31 38
         32 97
sp    => 33 5
         34 1
         35 1
         36 5
         37 0
   bp => 38 47
         39 155
         40 5
         41 1
---- memory (vram) ----
..... .....
.@... .....
..... .....
..... .....
..... .....

あれ、 reg_avram_get() の返り値)が 1 になるはずなのに……おかしい……。

(何回も動かして調べる)

分かりました。さっき修正した get_vram の修正がミスっていたようです。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -283,7 +283,7 @@ class Vm
           when String
             case arg1
             when /^\[bp\-(\d+)\]$/
-              stack_addr = $1.to_i
+              stack_addr = @bp - $1.to_i
               @mem.stack[stack_addr]
             else
               raise not_yet_impl("arg1", arg1)

再度実行すると、ちゃんと reg_a に 1 が入りました!

ステップ数を表示

さて、さっき「何回も動かして調べる」と書きました。

何をしてたかというと、

  • (1) 実行しつつメインメモリとにらめっこして、目的の箇所を探す (辿り着くまで Enter キーで進める)
  • (2) 目的の箇所で止めて状態を確認する
  • (3) おかしかったら、 Ctrl+C で止めて、VM のコードを修正して、 (1) に戻る

みたいなことを繰り返していました。

ここらへんから、デバッグがだんだんしんどくなってきます。 @pc がメインメモリの中で行ったり来たりするので 「目的の箇所を探す」がまず大変。 集中力が必要だし、見間違えて通り過ぎると「あぁ〜また最初から実行しないと……」 となります。

そこで、何か工夫しようということで、ステップ数を表示することにしました。 これならすぐできそう。

目的の箇所が何ステップ目かが分かれば、 2回目以降はステップ数を見ながら、たとえば 「120ステップのところまで進めて確認すればいいな」 という感じで作業できて、これだけでもだいぶ楽になります。

修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -132,6 +132,8 @@ class Vm
     @sp = stack_size - 1
     # ベースポインタ
     @bp = stack_size - 1
+
+    @step = 0
   end
 
   def set_sp(addr)
@@ -148,6 +150,8 @@ class Vm
     $stdin.gets
 
     loop do
+      @step += 1
+
       # operator
       op = @mem.main[@pc]
 
@@ -371,7 +375,7 @@ class Vm
   def dump_v2
     puts <<-EOB
 ================================
-#{ dump_reg() } zf(#{ @zf })
+#{ @step }: #{ dump_reg() } zf(#{ @zf })
 ---- memory (main) ----
 #{ @mem.dump_main(@pc) }
 ---- memory (stack) ----

これで実行すると下記のようになり、 さっきのデバッグで見ていた箇所は 62 ステップ目だな、というように分かるわけです。 これは改善ですね!

================================
62: reg_a(1) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      69   ["cp", "bp", "sp"]
      72   ["pop", "bp"]
      74   ["ret"]
      75 ["label", "count_alive"]
      77   ["push", "bp"]
      79   ["cp", "sp", "bp"]
      82   ["sub_sp", 1]
      84   ["cp", 0, "[bp-1]"]
      87   ["sub_sp", 1]
      89   ["push", "[bp+4]"]
      91   ["push", "[bp+3]"]
      93   ["push", "[bp+2]"]
      95   ["call", 41]
pc => 97   ["add_sp", 3]
      99   ["cp", "reg_a", "[bp-2]"]
      102   ["cp", "bp", "sp"]
      105   ["pop", "bp"]
      107   ["ret"]
      108 ["label", "main"]
      ...

さらに、こうすれば

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -311,7 +311,7 @@ class Vm
       end
 
       dump_v2()
-      $stdin.gets
+      $stdin.gets >= 62
       sleep 0.01
     end
   end

調べたいところまで早送りして、それ以降はステップ実行、 なんてこともできます!


VRAM からセルの生死の情報が取得できるようになったので、 生存カウントの続きに戻りましょう!


(2021-05-28 追記)

vm2gol v2 (58) _debug でブレークポイントを指定できるようにした

その後、ステップ 58 でさらに改良してブレークポイントを指定できるようにしました。 ちょっとした修正で済むので、このあたりで先にやっておけばよかったですね。