vm2gol v2 製作メモ(37) 完成!


はい、前回でとうとう 1回のターンが動くように……なってません! 1回のターンに必要な処理がもう1つ残ってます。 VRAM のバッファ領域に書き込んだ次世代の状態をメイン領域に戻す処理です。

やりましょう!


まずは、二重ループの部分が大きくなってきたので 別の関数に分けます。

分かりやすい diff にならなかったので 修正後のものを貼ります。

変数宣言と、 x, y の初期化と 二重ループの部分を make_next_gen() に抽出しました。

, ["func", "make_next_gen", ["w", "h"]
  , [
      ["var", "x"]
    , ["set", "x", 0]

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

    , ["var", "count"]
      // 注目しているセルの現世代の生死
    , ["var", "current_val"]
      // 注目しているセルの次世代の生死
    , ["var", "next_val"]

    , ["while", ["neq", "y", "h"]
      , [
          ["set", "x", 0]
        , ["while", ["neq", "x", "w"]
          , [
              ["call_set", "count", ["count_alive", "w", "h", "x", "y"]]
            , ["_cmt", "★ count_alive から戻った直後"]

            , ["_cmt", "★次世代の生死決定の直前"]

            , ["call_set", "current_val", ["vram_get", "w", "x", "y"]]

            , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]

            , ["_cmt", "★次世代の生死決定の直後"]

            , ["call", "vram_set_buf", "w", "x", "y", "next_val"]
            , ["_cmt", "★ vram_set_buf から戻った直後"]

            , ["set", "x", ["+", "x", 1]]
            ]
          ]
        , ["set", "y", ["+", "y", 1]]
        ]
      ]
    ]
  ]

, ["func", "main", []
  , [
    // ...

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

      // 初期状態の設定
    , ["call", "vram_set", "w", 1, 0, 1]
    , ["call", "vram_set", "w", 2, 1, 1]
    , ["call", "vram_set", "w", 0, 2, 1]
    , ["call", "vram_set", "w", 1, 2, 1]
    , ["call", "vram_set", "w", 2, 2, 1]

    , ["call", "make_next_gen", "w", "h"]
    ]
  ]

実行。

$ ./run.sh gol.vgt.json 
vgcg.rb:208:in `block in codegen_call': Not yet implemented ("w") (RuntimeError)
    from vgcg.rb:198:in `each'
    from vgcg.rb:198:in `codegen_call'
    from vgcg.rb:400:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:93:in `codegen_while'
    from vgcg.rb:408:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:93:in `codegen_while'
    from vgcg.rb:375:in `block in codegen_func_def'
    from vgcg.rb:356:in `each'
    from vgcg.rb:356:in `codegen_func_def'
    from vgcg.rb:398:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:427:in `codegen'
    from vgcg.rb:439:in `<main>'

うーむ、この期に及んでまだ出ますね……。 call のときに関数の引数の参照が解決できてないようです。 修正します。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -191,7 +191,7 @@ def codegen_exp(fn_arg_names, lvar_names, exp)
   alines
 end
 
-def codegen_call(lvar_names, stmt_rest)
+def codegen_call(fn_arg_names, lvar_names, stmt_rest)
   alines = []
 
   fn_name, *fn_args = stmt_rest
@@ -201,6 +201,9 @@ def codegen_call(lvar_names, stmt_rest)
       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}"
       when lvar_names.include?(fn_arg)
         lvar_addr = to_lvar_addr(lvar_names, fn_arg)
         alines << "  push #{lvar_addr}"
@@ -357,7 +360,7 @@ def codegen_func_def(rest)
     stmt_head, *stmt_rest = stmt
     case stmt_head
     when "call"
-      alines += codegen_call(lvar_names, stmt_rest)
+      alines += codegen_call(fn_arg_names, lvar_names, stmt_rest)
     when "call_set"
       alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     when "var"
@@ -397,7 +400,7 @@ def codegen_stmts(fn_arg_names, lvar_names, rest)
     when "func"
       alines += codegen_func_def(stmt_rest)
     when "call"
-      alines += codegen_call(lvar_names, stmt_rest)
+      alines += codegen_call(fn_arg_names, lvar_names, stmt_rest)
     when "call_set"
       alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     when "set"

一度動かして、問題なさそうなので、 replace_with_buf() を追加して、 make_next_gen() の次に呼び出します。

VRAM のメイン領域をバッファ領域の内容で置き換える処理です。これは割と簡単。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -268,6 +268,25 @@
     ]
   ]
 
+, ["func", "replace_with_buf", []
+  , [
+      ["var", "vi"]
+    , ["set", "vi", 0]
+
+    , ["var", "vi_buf"]
+    , ["var", "temp"]
+
+    , ["while", ["neq", "vi", 25]
+      , [
+          ["set", "vi_buf", ["+", "vi", 25]]
+        , ["set", "temp", "vram[vi_buf]"]
+        , ["set", "vram[vi]", "temp"]
+        , ["set", "vi", ["+", "vi", 1]]
+        ]
+      ]
+    ]
+  ]
+
 , ["func", "main", []
   , [
     //   ["var", "tmp"]
@@ -300,6 +319,7 @@
     , ["call", "vram_set", "w", 2, 2, 1]
 
     , ["call", "make_next_gen", "w", "h"]
+    , ["call", "replace_with_buf"]
     ]
   ]

実行!

vgvm.rb:293:in `block in start': Not yet implemented ("arg1") ("vi_buf") (RuntimeError)
    from vgvm.rb:152:in `loop'
    from vgvm.rb:152:in `start'
    from vgvm.rb:470:in `<main>'

修正!!

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -273,10 +273,20 @@ def codegen_set(fn_arg_names, lvar_names, rest)
       to_fn_arg_addr(fn_arg_names, rest[1])
     when lvar_names.include?(rest[1])
       to_lvar_addr(lvar_names, rest[1])
-    when /^vram\[(.+)\]$/ =~ rest[1]
+    when /^vram\[(\d+)\]$/ =~ rest[1]
       vram_addr = $1
       alines << "  get_vram #{vram_addr} reg_a"
       "reg_a"
+    when /^vram\[([a-z_][a-z0-9_]*)\]$/ =~ rest[1]
+      dest = $1
+      case
+      when lvar_names.include?(dest)
+        lvar_addr = to_lvar_addr(lvar_names, dest)
+        alines << "  get_vram #{ lvar_addr } reg_a"
+      else
+        raise not_yet_impl("rest", rest)
+      end
+      "reg_a"
     else
       raise not_yet_impl("set src_val", rest)
     end

場当たり的な感じがしますがもうちょっとなので逃げ切りたい!

vgvm.rb:264:in `block in start': Not yet implemented ("set_vram") ("[bp-3]") (RuntimeError)
    from vgvm.rb:152:in `loop'
    from vgvm.rb:152:in `start'
    from vgvm.rb:470:in `<main>'

修正!!

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -260,6 +260,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("set_vram", arg2)
           end

実行!!

================================
19517: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 1374]
pc => 02   ["exit"]
      03 ["label", "to_vi"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["sub_sp", 1]
      12   ["set_reg_a", "[bp+4]"]
      14   ["set_reg_b", "[bp+2]"]
      16   ["mult_ab"]
      17   ["cp", "reg_a", "[bp-1]"]
      20   ["sub_sp", 1]
      22   ["set_reg_a", "[bp-1]"]
      24   ["set_reg_b", "[bp+3]"]
      26   ["add_ab"]
      27   ["cp", "reg_a", "[bp-2]"]
      30   ["set_reg_a", "[bp-2]"]
      32   ["set_reg_b", "[bp+5]"]
---- memory (stack) ----
         41 0
         42 25
         43 47
         44 1473
         45 5
         46 5
         47 49
         48 2
sp bp => 49 0
---- memory (vram) ----
..... .....
..... @.@..
..... .@@..
..... .@...
..... .....
exit

グェーッッ! 全部死んだッ!!!

デバッグ! 修正!!

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -278,10 +278,10 @@ def codegen_set(fn_arg_names, lvar_names, rest)
       alines << "  get_vram #{vram_addr} reg_a"
       "reg_a"
     when /^vram\[([a-z_][a-z0-9_]*)\]$/ =~ rest[1]
-      dest = $1
+      var_name = $1
       case
-      when lvar_names.include?(dest)
-        lvar_addr = to_lvar_addr(lvar_names, dest)
+      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("rest", rest)

ここの前後ですでに dest という名前の変数を使っていて、 ここで dest に別の値を入れてしまうとまずいのでした *1

実行!!!

================================
19517: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 1374]
pc => 02   ["exit"]
      03 ["label", "to_vi"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["sub_sp", 1]
      12   ["set_reg_a", "[bp+4]"]
      14   ["set_reg_b", "[bp+2]"]
      16   ["mult_ab"]
      17   ["cp", "reg_a", "[bp-1]"]
      20   ["sub_sp", 1]
      22   ["set_reg_a", "[bp-1]"]
      24   ["set_reg_b", "[bp+3]"]
      26   ["add_ab"]
      27   ["cp", "reg_a", "[bp-2]"]
      30   ["set_reg_a", "[bp-2]"]
      32   ["set_reg_b", "[bp+5]"]
---- memory (stack) ----
         41 49
         42 25
         43 47
         44 1473
         45 5
         46 5
         47 49
         48 2
sp bp => 49 0
---- memory (vram) ----
..... .....
@.@.. @.@..
.@@.. .@@..
.@... .@...
..... .....
exit

よっしゃ!!!!


このままゴールまでいきましょう!!!!!!!!!!

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -318,8 +318,12 @@
     , ["call", "vram_set", "w", 1, 2, 1]
     , ["call", "vram_set", "w", 2, 2, 1]
 
-    , ["call", "make_next_gen", "w", "h"]
-    , ["call", "replace_with_buf"]
+    , ["while", ["eq", 0, 0]
+      , [
+          ["call", "make_next_gen", "w", "h"]
+        , ["call", "replace_with_buf"]
+        ]
+      ]
     ]
   ]

グワーッッッ!! 動いた!!! 動きました!!!!!! (めちゃくちゃ遅いけど!!!!)


いやー、できるかどうかよく分からないとこから始めて とうとうここまで辿り着きましたね……感無量……。

とりあえずここでゴールです! 目標は達成しました。

Mission accomplished. (TODO ランボー/怒りの脱出 のキャプチャ画像を貼る*2


次回、いくつか落ち穂拾い的な修正をやって いったん終わりにしようと思います。

*1:ここちょっと不自然ですが、不自然なのは下書きを書いた後で変数名を変えてしまったためです。実際に発生したことではありますし、おもしろかったので再現しておきました。

*2:貼りませんが

vm2gol v2 製作メモ(36) すべてのセルで繰り返し


前回までで、1つのセルについて 生存セル数をカウントして、 次世代の生死を決定して、 バッファ用の配列に書き込むところまでできました。

今回は、盤面のすべてのセルに対してそれを行うようにしましょう!! (ゴールが近づいてテンション上がってきた)


といっても単にループ回すだけです!!

まず、初期状態をグライダーにしましょう!!!

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -257,9 +257,11 @@
     , ["set", "y", 0]
 
       // 初期状態の設定
-    , ["call", "vram_set", "w", 1, 1, 1]
+    , ["call", "vram_set", "w", 1, 0, 1]
     , ["call", "vram_set", "w", 2, 1, 1]
-    , ["call", "vram_set", "w", 3, 1, 1]
+    , ["call", "vram_set", "w", 0, 2, 1]
+    , ["call", "vram_set", "w", 1, 2, 1]
+    , ["call", "vram_set", "w", 2, 2, 1]
 
     , ["var", "count"]

それから 「 生存セル数をカウントして、 次世代の生死を決定して、 バッファ領域に書き込む 」 部分をループで囲みます。

先に変数宣言をループで囲む範囲の外側に移動させて……

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -264,18 +264,18 @@
     , ["call", "vram_set", "w", 2, 2, 1]
 
     , ["var", "count"]
+      // 注目しているセルの現世代の生死
+    , ["var", "current_val"]
+      // 注目しているセルの次世代の生死
+    , ["var", "next_val"]
 
     , ["call_set", "count", ["count_alive", "w", "h", 2, 2]]
     , ["_cmt", "★ count_alive から戻った直後"]
 
     , ["_cmt", "★次世代の生死決定の直前"]
 
-      // 注目しているセルの現世代の生死
-    , ["var", "current_val"]
     , ["call_set", "current_val", ["vram_get", "w", "h", 2, 2]]
 
-      // 注目しているセルの次世代の生死
-    , ["var", "next_val"]
     , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]
 
     , ["_cmt", "★次世代の生死決定の直後"]

ハードコーディングしていた座標を変数に置き換えて……

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -269,18 +269,18 @@
       // 注目しているセルの次世代の生死
     , ["var", "next_val"]
 
-    , ["call_set", "count", ["count_alive", "w", "h", 2, 2]]
+    , ["call_set", "count", ["count_alive", "w", "h", "x", "y"]]
     , ["_cmt", "★ count_alive から戻った直後"]
 
     , ["_cmt", "★次世代の生死決定の直前"]
 
-    , ["call_set", "current_val", ["vram_get", "w", "h", 2, 2]]
+    , ["call_set", "current_val", ["vram_get", "w", "h", "x", "y"]]
 
     , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]
 
     , ["_cmt", "★次世代の生死決定の直後"]
 
-    , ["call", "vram_set_buf", "w", 2, 2, "next_val"]
+    , ["call", "vram_set_buf", "w", "x", "y", "next_val"]
     , ["_cmt", "★ vram_set_buf から戻った直後"]
     ]
   ]

二重ループで囲みます! (ループの中の部分のインデントは別のコミットで修正しました)

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -269,7 +269,12 @@
       // 注目しているセルの次世代の生死
     , ["var", "next_val"]
 
-    , ["call_set", "count", ["count_alive", "w", "h", "x", "y"]]
+    , ["while", ["neq", "y", "h"]
+      , [
+          ["set", "x", 0]
+        , ["while", ["neq", "x", "w"]
+          , [
+      ["call_set", "count", ["count_alive", "w", "h", "x", "y"]]
     , ["_cmt", "★ count_alive から戻った直後"]
 
     , ["_cmt", "★次世代の生死決定の直前"]
@@ -282,6 +287,13 @@
 
     , ["call", "vram_set_buf", "w", "x", "y", "next_val"]
     , ["_cmt", "★ vram_set_buf から戻った直後"]
+
+            , ["set", "x", ["+", "x", 1]]
+            ]
+          ]
+        , ["set", "y", ["+", "y", 1]]
+        ]
+      ]
     ]
   ]

どうじゃ?

$ ./run.sh gol.vgt.json 
vgcg.rb:410:in `block in codegen_stmts': Not yet implemented ("stmt_head") ("call_set") (RuntimeError)
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:93:in `codegen_while'
    from vgcg.rb:406:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:93:in `codegen_while'
    from vgcg.rb:375:in `block in codegen_func_def'
    from vgcg.rb:356:in `each'
    from vgcg.rb:356:in `codegen_func_def'
    from vgcg.rb:398:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:425:in `codegen'
    from vgcg.rb:437:in `<main>'

修正します! (codegen_func_def() からコピペ)

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -398,6 +398,8 @@ def codegen_stmts(fn_arg_names, lvar_names, rest)
       alines += codegen_func_def(stmt_rest)
     when "call"
       alines += codegen_call(lvar_names, stmt_rest)
+    when "call_set"
+      alines += codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
     when "set"
       alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
     when "case"

実行!!

================================
18992: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 1116]
pc => 02   ["exit"]
      03 ["label", "to_vi"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["sub_sp", 1]
      12   ["set_reg_a", "[bp+4]"]
      14   ["set_reg_b", "[bp+2]"]
      16   ["mult_ab"]
      17   ["cp", "reg_a", "[bp-1]"]
      20   ["sub_sp", 1]
      22   ["set_reg_a", "[bp-1]"]
      24   ["set_reg_b", "[bp+3]"]
      26   ["add_ab"]
      27   ["cp", "reg_a", "[bp-2]"]
      30   ["set_reg_a", "[bp-2]"]
      32   ["set_reg_b", "[bp+5]"]
---- memory (stack) ----
         41 0
         42 0
         43 5
         44 5
         45 5
         46 5
         47 49
         48 2
sp bp => 49 0
---- memory (vram) ----
.@... .....
..@.. @.@..
@@@.. .@...
..... .@...
..... .....
exit

グワーッ 謎! ナンデ??

(↓こうなるのが正しいのです)

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

ふーむ。

おかしいのは (2,2) のときの動作なので、x, y を (2,2) に固定して コメントを追加したりして (このときにいじったコードは割愛します) がんばってデバッグして、

================================
905: reg_a(0) reg_b(0) reg_c(0) zf(1)
---- memory (main) ----
      1283   ["_cmt", "call_set~~count_alive"]
      1285   ["call", 451]
      1287   ["add_sp", 4]
      1289   ["cp", "reg_a", "[bp-5]"]
      1292   ["_cmt", "★~count_alive~から戻った直後"]
      1294   ["_cmt", "★次世代の生死決定の直前"]
      1296   ["push", "[bp-4]"]
      1298   ["push", "[bp-3]"]
      1300   ["push", "[bp-2]"]
      1302   ["push", "[bp-1]"]
      1304   ["_cmt", "call_set~~vram_get"]
      1306   ["call", 119]
      1308   ["add_sp", 4]
      1310   ["cp", "reg_a", "[bp-6]"]
pc => 1313   ["_cmt", "★~current_val~をセットした直後"]
      1315   ["push", "[bp-5]"]
      1317   ["push", "[bp-6]"]
      1319   ["_cmt", "call_set~~calc_next_gen"]
      1321   ["call", 279]
      1323   ["add_sp", 2]
      1325   ["cp", "reg_a", "[bp-7]"]
      1328   ["_cmt", "★次世代の生死決定の直後"]
      1330   ["push", "[bp-7]"]
      1332   ["push", "[bp-4]"]
      1334   ["push", "[bp-3]"]
      1336   ["push", "[bp-1]"]
      1338   ["_cmt", "call~~vram_set_buf"]
      1340   ["call", 84]
      1342   ["add_sp", 4]
---- memory (stack) ----
         32 0
         33 15
         34 47
         35 1308
         36 5
         37 5
         38 2
         39 2
sp    => 40 2
         41 0 ... current_val
         42 2 ... count
         43 2 ... y?
         44 2 ... x?
         45 5
         46 5
   bp => 47 49
         48 2
---- memory (vram) ----
.@... .....
..@.. .....
@@@.. .....
..... .....
..... .....

vram_get() で (2,2) の現世代の生死を取得して ローカル変数 current_val にセットした直後です。 最初の世代の (2,2) は生きているので current_val が 1 になっているべきなのに 0 になっています。

ということは vram_get() がおかしい? とさらに追いかけて……

, ["call_set", "current_val", ["vram_get", "w", "h", "x", "y"]]

ここがミスってました。 vram_get の引数は w, x, y が正しい (h は不要)。 いやー、プログラムが大きくなってきたのでデバッグが大変……。

前々回(第34回) のときに気づいて修正していればここで苦労しなくてよかったのでした。 動作確認大事。 あと、関数呼び出し時の引数チェックがあれば……とか考えてしまいますね。 )

というわけで修正。

-    , ["call_set", "current_val", ["vram_get", "w", "h", "x", "y"]]
+    , ["call_set", "current_val", ["vram_get", "w", "x", "y"]]

再度実行!

================================
18959: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      00   ["call", 1116]
pc => 02   ["exit"]
      03 ["label", "to_vi"]
      05   ["push", "bp"]
      07   ["cp", "sp", "bp"]
      10   ["sub_sp", 1]
      12   ["set_reg_a", "[bp+4]"]
      14   ["set_reg_b", "[bp+2]"]
      16   ["mult_ab"]
      17   ["cp", "reg_a", "[bp-1]"]
      20   ["sub_sp", 1]
      22   ["set_reg_a", "[bp-1]"]
      24   ["set_reg_b", "[bp+3]"]
      26   ["add_ab"]
      27   ["cp", "reg_a", "[bp-2]"]
      30   ["set_reg_a", "[bp-2]"]
      32   ["set_reg_b", "[bp+5]"]
---- memory (stack) ----
         41 0
         42 0
         43 5
         44 5
         45 5
         46 5
         47 49
         48 2
sp bp => 49 0
---- memory (vram) ----
.@... .....
..@.. @.@..
@@@.. .@@..
..... .@...
..... .....
exit

やりました!!!! (最後まで全部実行した状態です)

総ステップ数がめちゃくちゃ増えましたね。 ステップ実行なしでも13秒くらいかかります。

次回でとうとう完成します!!!

vm2gol v2 製作メモ(35) 次世代の生死をバッファ領域に書き込み


周囲の生存セル数から次世代の生死を決定できるようになりました。 今回は、これを VRAM のバッファ領域に書き込む部分を作ります。

VRAM のメイン領域とバッファ領域は、アドレスが 25 ずれているので、 x, y 座標から算出したアドレスに 25 を足せば、 メイン領域と同じようにバッファ領域の読み書きができます。

前に作った vram_set() をコピペして、 vi に 25 を足す処理を追加して、 vram_set() のバッファ領域版を用意します。

25 というのがちょっとマジックナンバーっぽいですが、とりあえずハードコーディングです。 ここらへんは気分で。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -12,6 +12,20 @@
     ]
   ]
 
+, ["func", "vram_set_buf", ["w", "x", "y", "val"]
+  , [
+      ["var", "yw"]
+    , ["set", "yw", ["*", "y", "w"]]
+
+    , ["var", "vi"] // vram index
+    , ["set", "vi", ["+", "yw", "x"]]
+
+    , ["set", "vi", ["+", "vi", 25]]
+
+    , ["set", "vram[vi]", "val"]
+    ]
+  ]
+
 , ["func", "vram_get", ["w", "x", "y"]
   , [
       ["var", "yw"]

まずは vram_set_buf() 単体で確認してみましょうか。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -231,6 +231,15 @@
     ]
   ]
 
+, ["func", "test_vram_set_buf", []
+  , [
+      ["call", "vram_set_buf", 5, 2, 2, 1]
+    , ["_cmt", "★ (2,2) に 1 をセット"]
+    , ["call", "vram_set_buf", 5, 2, 2, 0]
+    , ["_cmt", "★ (2,2) に 0 をセット"]
+    ]
+  ]
+
 , ["func", "main", []
   , [
     //   ["var", "tmp"]
@@ -246,9 +255,11 @@
 
     //   ["call", "test_calc_next_gen"]
 
+      ["call", "test_vram_set_buf"]
+
     // ----------------
 
-      ["var", "w"] // 盤面の幅
+    , ["var", "w"] // 盤面の幅
     , ["set", "w", 5]
     , ["var", "h"] // 盤面の高さ
     , ["set", "h", 5]

(2,2) に 1 をセットした直後。 ダンプ表示の「memory (vram)」で結果が確認できます。

================================
34: reg_a(37) reg_b(25) reg_c(0) zf(0)
---- memory (main) ----
      1063   ["_cmt", "★~生-8~→~死になるはず"]
      1065   ["cp", "bp", "sp"]
      1068   ["pop", "bp"]
      1070   ["ret"]
      1071 ["label", "test_vram_set_buf"]
      1073   ["push", "bp"]
      1075   ["cp", "sp", "bp"]
      1078   ["push", 1]
      1080   ["push", 2]
      1082   ["push", 2]
      1084   ["push", 5]
      1086   ["_cmt", "call~~vram_set_buf"]
      1088   ["call", 41]
      1090   ["add_sp", 4]
pc => 1092   ["_cmt", "★~(2,2)~に~1~をセット"]
      1094   ["push", 0]
      1096   ["push", 2]
      1098   ["push", 2]
      1100   ["push", 5]
      1102   ["_cmt", "call~~vram_set_buf"]
      1104   ["call", 41]
      1106   ["add_sp", 4]
      1108   ["_cmt", "★~(2,2)~に~0~をセット"]
      1110   ["cp", "bp", "sp"]
      1113   ["pop", "bp"]
      1115   ["ret"]
      1116 ["label", "main"]
      1118   ["push", "bp"]
      1120   ["cp", "sp", "bp"]
---- memory (stack) ----
         37 37
         38 10
         39 45
         40 1090
         41 5
         42 2
         43 2
         44 1
sp bp => 45 47
         46 1127
         47 49
         48 2
         49 0
---- memory (vram) ----
..... .....
..... .....
..... ..@..
..... .....
..... .....

(2,2) に 0 をセットした直後。

================================
62: reg_a(37) reg_b(25) reg_c(0) zf(0)
---- memory (main) ----
      1078   ["push", 1]
      1080   ["push", 2]
      1082   ["push", 2]
      1084   ["push", 5]
      1086   ["_cmt", "call~~vram_set_buf"]
      1088   ["call", 41]
      1090   ["add_sp", 4]
      1092   ["_cmt", "★~(2,2)~に~1~をセット"]
      1094   ["push", 0]
      1096   ["push", 2]
      1098   ["push", 2]
      1100   ["push", 5]
      1102   ["_cmt", "call~~vram_set_buf"]
      1104   ["call", 41]
      1106   ["add_sp", 4]
pc => 1108   ["_cmt", "★~(2,2)~に~0~をセット"]
      1110   ["cp", "bp", "sp"]
      1113   ["pop", "bp"]
      1115   ["ret"]
      1116 ["label", "main"]
      1118   ["push", "bp"]
      1120   ["cp", "sp", "bp"]
      1123   ["_cmt", "call~~test_vram_set_buf"]
      1125   ["call", 1073]
      1127   ["add_sp", 0]
      1129   ["sub_sp", 1]
      1131   ["cp", 5, "[bp-1]"]
      1134   ["sub_sp", 1]
      1136   ["cp", 5, "[bp-2]"]
---- memory (stack) ----
         37 37
         38 10
         39 45
         40 1106
         41 5
         42 2
         43 2
         44 0
sp bp => 45 47
         46 1127
         47 49
         48 2
         49 0
---- memory (vram) ----
..... .....
..... .....
..... .....
..... .....
..... .....

問題ないですね!

確認が済んだので test_vram_set_buf() の呼び出しを無効化して、と。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -255,11 +255,11 @@
 
     //   ["call", "test_calc_next_gen"]
 
-      ["call", "test_vram_set_buf"]
+    //   ["call", "test_vram_set_buf"]
 
     // ----------------
 
-    , ["var", "w"] // 盤面の幅
+      ["var", "w"] // 盤面の幅
     , ["set", "w", 5]
     , ["var", "h"] // 盤面の高さ
     , ["set", "h", 5]

では、前回までに作った部分(次世代の生死を決定する)と くっつけて、次世代の生死がバッファ領域に書き込まれるようにしましょう。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -168,6 +168,7 @@
 
     , ["_cmt", "★次世代の生死決定の直後"]
 
+    , ["return", "next_val"]
     ]
   ]
 
@@ -271,12 +272,17 @@
     , ["set", "y", 0]
 
       // 初期状態の設定
-    , ["call", "vram_set", "w", 2, 2, 1]
     , ["call", "vram_set", "w", 1, 1, 1]
-    , ["call", "vram_set", "w", 1, 2, 1]
-    , ["call", "vram_set", "w", 1, 3, 1]
+    , ["call", "vram_set", "w", 2, 1, 1]
+    , ["call", "vram_set", "w", 3, 1, 1]
+
+    , ["var", "next_val"]
+
+    , ["call_set", "next_val", ["count_alive", "w", "h", 2, 2]]
+    , ["_cmt", "★ count_alive から戻った直後"]
 
-    , ["call", "count_alive", "w", "h", 2, 2]
+    , ["call", "vram_set_buf", "w", 2, 2, "next_val"]
+    , ["_cmt", "★ vram_set_buf の直後"]
     ]
   ]

実行。

================================
635: reg_a(37) reg_b(25) reg_c(0) zf(1)
---- memory (main) ----
      1194   ["push", "[bp-2]"]
      1196   ["push", "[bp-1]"]
      1198   ["_cmt", "call_set~~count_alive"]
      1200   ["call", 418]
      1202   ["add_sp", 4]
      1204   ["cp", "reg_a", "[bp-5]"]
      1207   ["_cmt", "★~count_alive~から戻った直後"]
      1209   ["push", "[bp-5]"]
      1211   ["push", 2]
      1213   ["push", 2]
      1215   ["push", "[bp-1]"]
      1217   ["_cmt", "call~~vram_set_buf"]
      1219   ["call", 41]
      1221   ["add_sp", 4]
pc => 1223   ["_cmt", "★~vram_set_buf~から戻った直後"]
      1225   ["cp", "bp", "sp"]
      1228   ["pop", "bp"]
      1230   ["ret"]
---- memory (stack) ----
         34 37
         35 10
         36 47
         37 1221
         38 5
         39 2
         40 2
         41 1
sp    => 42 1
         43 0
         44 0
         45 5
         46 5
   bp => 47 49
         48 2
         49 0
---- memory (vram) ----
..... .....
.@@@. .....
..... ..@..
..... .....
..... .....

おおぉ〜〜、まだ 1セルだけですが、ふわ〜っとライフゲーム臭が漂ってきました!! いいぞいいぞ〜!


次に進む前にリファクタリングしておきます。

VRAM のアドレス vi に変換する部分です。

  ["var", "yw"]
, ["set", "yw", ["*", "y", "w"]]

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

これが繰り返し出てくるので、関数に抽出しておきます。

to_vi() を追加して……

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -1,5 +1,18 @@
 ["stmts"
 
+, ["func", "to_vi", ["w", "x", "y", "offset"]
+  , [
+      ["var", "yw"]
+    , ["set", "yw", ["*", "y", "w"]]
+
+    , ["var", "vi"] // vram index
+    , ["set", "vi", ["+", "yw", "x"]]
+    , ["set", "vi", ["+", "vi", "offset"]]
+
+    , ["return", "vi"]
+    ]
+  ]
+
 , ["func", "vram_set", ["w", "x", "y", "val"]
   , [
       ["var", "yw"]

to_vi() を使って vi を求めるように置き換えます。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -15,38 +15,24 @@
 
 , ["func", "vram_set", ["w", "x", "y", "val"]
   , [
-      ["var", "yw"]
-    , ["set", "yw", ["*", "y", "w"]]
-
-    , ["var", "vi"] // vram index
-    , ["set", "vi", ["+", "yw", "x"]]
-
+      ["var", "vi"] // vram index
+    , ["call_set", "vi", ["to_vi", "w", "x", "y", 0]]
     , ["set", "vram[vi]", "val"]
     ]
   ]
 
 , ["func", "vram_set_buf", ["w", "x", "y", "val"]
   , [
-      ["var", "yw"]
-    , ["set", "yw", ["*", "y", "w"]]
-
-    , ["var", "vi"] // vram index
-    , ["set", "vi", ["+", "yw", "x"]]
-
-    , ["set", "vi", ["+", "vi", 25]]
-
+      ["var", "vi"] // vram index
+    , ["call_set", "vi", ["to_vi", "w", "x", "y", 25]]
     , ["set", "vram[vi]", "val"]
     ]
   ]
 
 , ["func", "vram_get", ["w", "x", "y"]
   , [
-      ["var", "yw"]
-    , ["set", "yw", ["*", "y", "w"]]
-
-    , ["var", "vi"] // vram index
-    , ["set", "vi", ["+", "yw", "x"]]
-
+      ["var", "vi"] // vram index
+    , ["call_set", "vi", ["to_vi", "w", "x", "y", 0]]
     , ["return", "vram[vi]"]
     ]
   ]

もうひとつ雑に書いたところをリファクタリングしておきます。

次世代の生死決定の処理を count_alive() の中に書いてしまった (特に意図があったわけではなく、 流れでなんとなくそこに書いた、というだけです……)ため、

  • 生存セル数のカウントと生死決定という、2つのことを行っている
  • 関数名と処理内容が一致していない

という、良くない状態になってますので、これを解消しましょう。

count_alive() では名前の通り生存セル数のカウントだけを行い、 生存セル数を返すようにします。

生死決定の部分は数行だけなので、いったん main 関数に移動させておきます。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -153,21 +153,7 @@
 
     , ["_cmt", "★count_aliveの最後"]
 
-      // ----------------
-
-    , ["_cmt", "★次世代の生死決定の直前"]
-
-      // 注目しているセルの現世代の生死
-    , ["var", "current_val"]
-    , ["call_set", "current_val", ["vram_get", "w", "h", "x", "y"]]
-
-      // 注目しているセルの次世代の生死
-    , ["var", "next_val"]
-    , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]
-
-    , ["_cmt", "★次世代の生死決定の直後"]
-
-    , ["return", "next_val"]
+    , ["return", "count"]
     ]
   ]
 
@@ -275,11 +261,23 @@
     , ["call", "vram_set", "w", 2, 1, 1]
     , ["call", "vram_set", "w", 3, 1, 1]
 
-    , ["var", "next_val"]
+    , ["var", "count"]
 
-    , ["call_set", "next_val", ["count_alive", "w", "h", 2, 2]]
+    , ["call_set", "count", ["count_alive", "w", "h", 2, 2]]
     , ["_cmt", "★ count_alive から戻った直後"]
 
+    , ["_cmt", "★次世代の生死決定の直前"]
+
+      // 注目しているセルの現世代の生死
+    , ["var", "current_val"]
+    , ["call_set", "current_val", ["vram_get", "w", "h", 2, 2]]
+
+      // 注目しているセルの次世代の生死
+    , ["var", "next_val"]
+    , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]
+
+    , ["_cmt", "★次世代の生死決定の直後"]
+
     , ["call", "vram_set_buf", "w", 2, 2, "next_val"]
     , ["_cmt", "★ vram_set_buf から戻った直後"]
     ]

これで count_alive という名前の通りの処理だけを行う関数になりました。


今回はコード生成器や VM の修正はありませんでしたね。 完成まであとちょっと!!

vm2gol v2 製作メモ(34) 次世代の生死を決定


もうだんだんライフゲームそのもののコーディングに移ってきたので、

cp 33_adjust_index_2.vgt.json gol.vgt.json

として、以後はこの gol.vgt.json に対して修正を加えていきます。


座標補正ができて、その前に生存セルのカウントも作っていたので、 今回は生存セルの数によって次世代の生死を決定する部分を作ります。

Ruby のコードではこのように書いていました:

if $grid[y][x] == 0
  if n == 3
    $buf[y][x] = 1
  else
    $buf[y][x] = 0 # 死んだまま
  end
else
  if n <= 1
    $buf[y][x] = 0
  elsif n >= 4
    $buf[y][x] = 0
  else
    $buf[y][x] = 1
  end
end

これをそのまま vgt コードに書き直すと

  // 現在のセルの生死
  ["var", "current"]
, ["call_set", "current"
             , ["vram_get", "w", "h", "x", "y"]
  ]

  // 次世代の生死
, ["var", "next_val"]

, ["case"
  , [["eq", "current", 0]
      ["case"
      , [["eq", "n", 3]
        , ["set", "next_val", 1]]
      , [["eq", 0, 0]
        , ["set", "next_val", 0]]
      ]
    ]
  , [["eq", 0, 0]
      ["case"
      , [["le", "n", 1]
        , ["set", "next_val", 0]]
      , [["ge", "n", 4]
        , ["set", "next_val", 0]]
      , [["eq", 0, 0]
        , ["set", "next_val", 1]]
      ]
    ]
  ]

こうなるんですが、まだ le とか ge を実装してないんですよね。

ここで実装してしまってもいいんですが、 今あるものでどうにかできないかと思って変形してみました。

生存カウントが取りうる値は 0〜8 なので、 「1以下」は「0, 1 のいずれか」 「4以上」は「4, 5, 6, 7, 8 のいずれか」 に変更できます。

こうすると eq だけでいけますね。

// case 文の部分だけ

["case"
, [["eq", "current", 0]
    // 現在の状態が死の場合
  , ["case"
    , [["eq", "n", 3]
      , ["set", "next_val", 1]]
    , [["eq", 0, 0]
      , ["set", "next_val", 0]]
    ]
  ]
, [["eq", 0, 0]
    // 現在の状態が生の場合
  , ["case"
    , [["eq", "n", 0]
      , ["set", "next_val", 0]]
    , [["eq", "n", 1]
      , ["set", "next_val", 0]]
    , [["eq", "n", 4]
      , ["set", "next_val", 0]]
    , [["eq", "n", 5]
      , ["set", "next_val", 0]]
    , [["eq", "n", 6]
      , ["set", "next_val", 0]]
    , [["eq", "n", 7]
      , ["set", "next_val", 0]]
    , [["eq", "n", 8]
      , ["set", "next_val", 0]]
    , [["eq", 0, 0] // 生存カウントが 2 または 3 の場合
      , ["set", "next_val", 1]]
    ]
  ]
]

うーん、 eq だけでなんとかなりますが、 なんだかやぼったいですね。 もうちょっとなんとかならないか……。

( とはいえ、これで正しく動きますし、後からでも修正できるので、 このまま先に進んでも良かったかなという気も。 )

条件をじっくり見てみます。 よく見ると、 「現在の状態が生の場合」 「現在の状態が死の場合」 の両方を合わせても、 次世代が生になるのは3通りしかないことに気づきます。

死 → 生: 生存セル数が 3 の場合だけ
生 → 生: 生存セル数が 2 または 3 の場合だけ

これ以外の場合、次世代は死です。

なるほど……。

現在の状態が生の場合、「0, 1, 4, 5, 6, 7, 8 だったら死、それ以外は生」 じゃなくて「2, 3 だったら生、それ以外は死」 とすると、

["case"
, [["eq", "current", 0]
    // 現在の状態が死の場合
  , ["case"
    , [["eq", "n", 3]
      , ["set", "next_val", 1]]
    , [["eq", 0, 0]
      , ["set", "next_val", 0]]
    ]
  ]
, [["eq", 0, 0]
    // 現在の状態が生の場合
  , ["case"
    , [["eq", "n", 2]
      , ["set", "next_val", 1]]
    , [["eq", "n", 3]
      , ["set", "next_val", 1]]
    , [["eq", 0, 0]
      , ["set", "next_val", 0]]
    ]
  ]
]

やぼったいのがだいぶ解消されました。

さらに、 next_val のデフォルト値を死にしてしまえば、 else 節も省けて 「生になるのが3通り、それ以外は死」を素直に表現したコードになる気がします。

  ["var", "next_val"]
, ["set", "next_val", 0]

, ["case"
  , [["eq", "current", 0]
      // 現在の状態が死の場合
    , ["case"
      , [["eq", "n", 3]
        , ["set", "next_val", 1]]
      ]
    ]
  , [["eq", 0, 0]
      // 現在の状態が生の場合
    , ["case"
      , [["eq", "n", 2]
        , ["set", "next_val", 1]]
      , [["eq", "n", 3]
        , ["set", "next_val", 1]]
      ]
    ]
  ]

いいんじゃないでしょうか。

gol.vgt.json に組み込んで動かします。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -113,6 +113,38 @@
     , ["set", "count", ["+", "count", "tmp"]]
 
     , ["_cmt", "★count_aliveの最後"]
+
+      // ----------------
+
+    , ["_cmt", "★次世代の生死決定の直前"]
+
+      // 注目しているセルの現世代の生死
+    , ["var", "current_val"]
+    , ["call_set", "current_val", ["vram_get", "w", "h", "x", "y"]]
+
+      // 注目しているセルの次世代の生死
+    , ["var", "next_val"]
+    , ["set", "next_val", 0]
+
+    , ["case"
+      , [["eq", "current_val", 0]
+        , ["case"
+          , [["eq", "count", 3]
+            , ["set", "next_val", 1]]
+          ]
+        ]
+      , [["eq", 0, 0]
+        , ["case"
+          , [["eq", "count", 2]
+            , ["set", "next_val", 1]]
+          , [["eq", "count", 3]
+            , ["set", "next_val", 1]]
+          ]
+        ]
+      ]
+
+    , ["_cmt", "★次世代の生死決定の直後"]
+
     ]
   ]

実行。

$ ./run.sh gol.vgt.json 
vgcg.rb:408:in `block in codegen_stmts': Not yet implemented ("stmt_head") ("case") (RuntimeError)
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:47:in `block in codegen_case'
    from vgcg.rb:29:in `each'
    from vgcg.rb:29:in `codegen_case'
    from vgcg.rb:373:in `block in codegen_func_def'
    from vgcg.rb:356:in `each'
    from vgcg.rb:356:in `codegen_func_def'
    from vgcg.rb:398:in `block in codegen_stmts'
    from vgcg.rb:394:in `each'
    from vgcg.rb:394:in `codegen_stmts'
    from vgcg.rb:423:in `codegen'
    from vgcg.rb:435:in `<main>'

case文が入れ子になっていて、 codegen_case() => codegen_stmts() => codegen_case() という流れで呼び出そうとして、 codegen_stmts() から codegen_case() が呼べなくてエラーになっています。

codegen_func_def() から持ってきて codegen_stmts() にコピー:

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -400,6 +400,8 @@ def codegen_stmts(fn_arg_names, lvar_names, rest)
       alines += codegen_call(lvar_names, stmt_rest)
     when "set"
       alines += codegen_set(fn_arg_names, lvar_names, stmt_rest)
+    when "case"
+      alines += codegen_case(fn_arg_names, lvar_names, stmt_rest)
     when "while"
       alines += codegen_while(fn_arg_names, lvar_names, stmt_rest)
     when "_cmt"

雑にやってますがたぶん大丈夫だろう、という感覚はあります。


さて、実行できるようになったところでこれから動作確認をやっていくわけですが。

動かしてみると、次世代の生死決定の部分に辿り着くまでに 459 ステップかかりました。

================================
459: reg_a(0) reg_b(0) reg_c(0) zf(1)
---- memory (main) ----
      496   ["cp", "reg_a", "[bp-1]"]
      499   ["_cmt", "右下"]
      501   ["push", "[bp-5]"]
      503   ["push", "[bp-3]"]
      505   ["push", "[bp+2]"]
      507   ["_cmt", "call_set~~vram_get"]
      509   ["call", 41]
      511   ["add_sp", 3]
      513   ["cp", "reg_a", "[bp-6]"]
      516   ["set_reg_a", "[bp-1]"]
      518   ["set_reg_b", "[bp-6]"]
      520   ["add_ab"]
      521   ["cp", "reg_a", "[bp-1]"]
      524   ["_cmt", "★count_aliveの最後"]
pc => 526   ["_cmt", "★次世代の生死決定の直前"]
      528   ["sub_sp", 1]
      530   ["push", "[bp+5]"]
      532   ["push", "[bp+4]"]

...

さすがにこのくらいの規模になってくると エンターキー押しっぱなし方式は辛いので、 ステップ実行開始の位置を適宜修正しながら作業します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -313,8 +313,8 @@ class Vm
       end
 
       dump_v2()
-      $stdin.gets if @step >= 0
-      sleep 0.01
+      $stdin.gets if @step >= 450
+      # sleep 0.01
     end
   end

sleep も外すと一瞬でたどり着きますね。現代のコンピュータはすごい……。


で、動作確認です。

中心のセルに注目して、そのセルの生死と 周囲の生存セルの数を変えて期待する動作になっているか確認していきます。

まずは注目するセル (2, 2) の現世代が死、周囲の生存セル数が 0 の場合。

      // 初期状態の設定
      // 設定なし → すべてのセルが死

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

この場合は次世代も死ですね。

================================
516: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      674 ["label", "end_eq_12"]
      676   ["set_reg_b", 1]
      678   ["compare"]
      679   ["jump_eq", 692]
      681   ["jump", 699]
      683 ["label", "when_10_0"]
      685   ["cp", 1, "[bp-8]"]
      688   ["jump", 699]
      690 ["label", "when_10_1"]
      692   ["cp", 1, "[bp-8]"]
      695   ["jump", 699]
      697 ["label", "end_case_10"]
      699   ["jump", 703]
      701 ["label", "end_case_5"]
pc => 703   ["_cmt", "★次世代の生死決定の直後"]
      705   ["cp", "bp", "sp"]
      708   ["pop", "bp"]
      710   ["ret"]
      711 ["label", "main"]
      713   ["push", "bp"]
      715   ["cp", "sp", "bp"]
      718   ["sub_sp", 1]
      720   ["cp", 5, "[bp-1]"]
      723   ["sub_sp", 1]
      725   ["cp", 5, "[bp-2]"]
      728   ["sub_sp", 1]
      730   ["cp", 0, "[bp-3]"]
      733   ["sub_sp", 1]
---- memory (stack) ----
         21 0
         22 15
         23 10
         24 37
         25 542
         26 5
         27 5
         28 2
sp    => 29 0 ... next_val
         30 0 ... current_val
         31 0
         32 3
         33 1
         34 3
         35 1
         36 0 ... count
   bp => 37 47

next_val が 0 になっているので、OKです。


次は死から生になるパターンを見てみましょう。

(1, 1), (2, 1), (3,1) を生にしてみます。

      // 初期状態の設定
    , ["call", "vram_set", "w", 1, 1, 1]
    , ["call", "vram_set", "w", 2, 1, 1]
    , ["call", "vram_set", "w", 3, 1, 1]

    , ["call", "count_alive", "w", "h", 2, 2]
================================
586: reg_a(1) reg_b(1) reg_c(0) zf(1)
---- memory (main) ----
      674 ["label", "end_eq_12"]
      676   ["set_reg_b", 1]
      678   ["compare"]
      679   ["jump_eq", 692]
      681   ["jump", 699]
      683 ["label", "when_10_0"]
      685   ["cp", 1, "[bp-8]"]
      688   ["jump", 699]
      690 ["label", "when_10_1"]
      692   ["cp", 1, "[bp-8]"]
      695   ["jump", 699]
      697 ["label", "end_case_10"]
      699   ["jump", 703]
      701 ["label", "end_case_5"]
pc => 703   ["_cmt", "★次世代の生死決定の直後"]
      705   ["cp", "bp", "sp"]
      708   ["pop", "bp"]
      710   ["ret"]
      711 ["label", "main"]
      713   ["push", "bp"]
      715   ["cp", "sp", "bp"]
      718   ["sub_sp", 1]
      720   ["cp", 5, "[bp-1]"]
      723   ["sub_sp", 1]
      725   ["cp", 5, "[bp-2]"]
      728   ["sub_sp", 1]
      730   ["cp", 0, "[bp-3]"]
      733   ["sub_sp", 1]
---- memory (stack) ----
         21 0
         22 15
         23 10
         24 37
         25 542
         26 5
         27 5
         28 2
sp    => 29 1 ... next_val
         30 0
         31 0
         32 3
         33 1
         34 3
         35 1
         36 3
   bp => 37 47

生になりました!


生 → 死 のパターン

    // 初期状態の設定
    , ["call", "vram_set", "w", 2, 2, 1]

    , ["call", "count_alive", "w", "h", 2, 2]
================================
539: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      674 ["label", "end_eq_12"]
      676   ["set_reg_b", 1]
      678   ["compare"]
      679   ["jump_eq", 692]
      681   ["jump", 699]
      683 ["label", "when_10_0"]
      685   ["cp", 1, "[bp-8]"]
      688   ["jump", 699]
      690 ["label", "when_10_1"]
      692   ["cp", 1, "[bp-8]"]
      695   ["jump", 699]
      697 ["label", "end_case_10"]
      699   ["jump", 703]
      701 ["label", "end_case_5"]
pc => 703   ["_cmt", "★次世代の生死決定の直後"]
      705   ["cp", "bp", "sp"]
      708   ["pop", "bp"]
      710   ["ret"]
      711 ["label", "main"]
      713   ["push", "bp"]
      715   ["cp", "sp", "bp"]
      718   ["sub_sp", 1]
      720   ["cp", 5, "[bp-1]"]
      723   ["sub_sp", 1]
      725   ["cp", 5, "[bp-2]"]
      728   ["sub_sp", 1]
      730   ["cp", 0, "[bp-3]"]
      733   ["sub_sp", 1]
---- memory (stack) ----
         21 0
         22 15
         23 10
         24 37
         25 542
         26 5
         27 5
         28 2
sp    => 29 0 ... next_val
         30 0
         31 0
         32 3
         33 1
         34 3
         35 1
         36 0
   bp => 37 47

いい感じですね!


生 → 生 のパターン

      // 初期状態の設定
    , ["call", "vram_set", "w", 2, 2, 1]
    , ["call", "vram_set", "w", 1, 1, 1]
    , ["call", "vram_set", "w", 1, 2, 1]
    , ["call", "vram_set", "w", 1, 3, 1]

    , ["call", "count_alive", "w", "h", 2, 2]
================================
609: reg_a(1) reg_b(1) reg_c(0) zf(1)
---- memory (main) ----
      674 ["label", "end_eq_12"]
      676   ["set_reg_b", 1]
      678   ["compare"]
      679   ["jump_eq", 692]
      681   ["jump", 699]
      683 ["label", "when_10_0"]
      685   ["cp", 1, "[bp-8]"]
      688   ["jump", 699]
      690 ["label", "when_10_1"]
      692   ["cp", 1, "[bp-8]"]
      695   ["jump", 699]
      697 ["label", "end_case_10"]
      699   ["jump", 703]
      701 ["label", "end_case_5"]
pc => 703   ["_cmt", "★次世代の生死決定の直後"]
      705   ["cp", "bp", "sp"]
      708   ["pop", "bp"]
      710   ["ret"]
      711 ["label", "main"]
      713   ["push", "bp"]
      715   ["cp", "sp", "bp"]
      718   ["sub_sp", 1]
      720   ["cp", 5, "[bp-1]"]
      723   ["sub_sp", 1]
      725   ["cp", 5, "[bp-2]"]
      728   ["sub_sp", 1]
      730   ["cp", 0, "[bp-3]"]
      733   ["sub_sp", 1]
---- memory (stack) ----
         21 0
         22 15
         23 10
         24 37
         25 542
         26 5
         27 5
         28 2
sp    => 29 1 ... next_val
         30 0
         31 0
         32 3
         33 1
         34 3
         35 1
         36 3
   bp => 37 47

これもヨシ!

( ヨシ! とか言ってますが、実はバグがあって、確認ミスしています。 ここは気づかなかったふりをしていったん続けます。 ミスは起こるよということで…… 次の次の回(第36回) でバグが発覚して修正されます )


ひとまず代表的なパターンだけやってみましたが、 不安なので一通り確認しておきます。

とは書いたものの、め、めんどくさい……あ、普通に関数として抽出してしまえばいいのか。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -45,6 +45,32 @@
     ]
   ]
 
+, ["func", "calc_next_gen", ["current_val", "count"]
+  , [
+      // 注目しているセルの次世代の生死
+      ["var", "next_val"]
+    , ["set", "next_val", 0]
+
+    , ["case"
+      , [["eq", "current_val", 0]
+        , ["case"
+          , [["eq", "count", 3]
+            , ["set", "next_val", 1]]
+          ]
+        ]
+      , [["eq", 0, 0]
+        , ["case"
+          , [["eq", "count", 2]
+            , ["set", "next_val", 1]]
+          , [["eq", "count", 3]
+            , ["set", "next_val", 1]]
+          ]
+        ]
+      ]
+    , ["return", "next_val"]
+    ]
+  ]
+
 , ["func", "count_alive", ["w", "h", "x", "y"]
   , [
       ["var", "count"]
@@ -124,24 +150,7 @@
 
       // 注目しているセルの次世代の生死
     , ["var", "next_val"]
-    , ["set", "next_val", 0]
-
-    , ["case"
-      , [["eq", "current_val", 0]
-        , ["case"
-          , [["eq", "count", 3]
-            , ["set", "next_val", 1]]
-          ]
-        ]
-      , [["eq", 0, 0]
-        , ["case"
-          , [["eq", "count", 2]
-            , ["set", "next_val", 1]]
-          , [["eq", "count", 3]
-            , ["set", "next_val", 1]]
-          ]
-        ]
-      ]
+    , ["call_set", "next_val", ["calc_next_gen", "current_val", "count"]]
 
     , ["_cmt", "★次世代の生死決定の直後"]

ここまで来るともう普通の、いつもやってる高級言語リファクタリングですね。

テスト用の関数を用意して、 calc_next_gen() のテストだけ全パターンやってしまいます。

ちゃんとした言語で書いてたら 「ここまでしっかりテストしなくてもええやろ」って思ってしまうところですが、 怪しいコード生成器とアセンブラで作った機械語コードを うさんくさいVMの上で動かしているので まったく油断できません。

, ["func", "test_calc_next_gen", []
  , [
      ["var", "next_val"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 0]]
    , ["_cmt", "★ 死-0 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 1]]
    , ["_cmt", "★ 死-1 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 2]]
    , ["_cmt", "★ 死-2 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 3]]
    , ["_cmt", "★ 死-3 → 生になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 4]]
    , ["_cmt", "★ 死-4 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 5]]
    , ["_cmt", "★ 死-5 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 6]]
    , ["_cmt", "★ 死-6 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 7]]
    , ["_cmt", "★ 死-7 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 0, 8]]
    , ["_cmt", "★ 死-8 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 0]]
    , ["_cmt", "★ 生-0 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 1]]
    , ["_cmt", "★ 生-1 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 2]]
    , ["_cmt", "★ 生-2 → 生になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 3]]
    , ["_cmt", "★ 生-3 → 生になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 4]]
    , ["_cmt", "★ 生-4 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 5]]
    , ["_cmt", "★ 生-5 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 6]]
    , ["_cmt", "★ 生-6 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 7]]
    , ["_cmt", "★ 生-7 → 死になるはず"]

    , ["call_set", "next_val", ["calc_next_gen", 1, 8]]
    , ["_cmt", "★ 生-8 → 死になるはず"]
    ]
  ]

, ["func", "main", []
  , [
    // ...

    ["call", "test_calc_next_gen"]

    // ...

すべて確認した結果、問題なさそうでした!

確認が済んだので test_calc_next_gen() の呼び出し箇所をコメントアウトしておきます。

--- a/gol.vgt.json
+++ b/gol.vgt.json
@@ -230,11 +230,11 @@
     // , ["call_set", "tmp", ["adjust_index", 5, 1]]
     // , ["_cmt", "★ 座標補正の確認 補正なし: 1 になるべき"]
 
-      ["call", "test_calc_next_gen"]
+    //   ["call", "test_calc_next_gen"]
 
     // ----------------
 
-    , ["var", "w"] // 盤面の幅
+      ["var", "w"] // 盤面の幅
     , ["set", "w", 5]
     , ["var", "h"] // 盤面の高さ
     , ["set", "h", 5]

完成までもうちょっと!!

vm2gol v2 製作メモ(33) 座標の補正 (2)


前回の座標補正の続き……の前に、 先に引っかかるところを潰しておきます。

codegen_return() でローカル変数を返そうとすると 参照の解決ができずにエラーになるので、修正。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -307,8 +307,8 @@ def codegen_return(lvar_names, stmt_rest)
   when Integer
     alines << "  set_reg_a #{retval}"
   when String
-    case retval
-    when /^vram\[([a-z0-9_]+)\]$/
+    case
+    when /^vram\[([a-z0-9_]+)\]$/ =~ retval
       var_name = $1
       case
       when lvar_names.include?(var_name)
@@ -317,6 +317,9 @@ def codegen_return(lvar_names, stmt_rest)
       else
         raise not_yet_impl("retval", retval)
       end
+    when lvar_names.include?(retval)
+      lvar_addr = to_lvar_addr(lvar_names, retval)
+      alines << "  cp #{lvar_addr} reg_a"
     else
       raise not_yet_impl("retval", retval)
     end

はい、では本題に戻りましょう。

前回は左端を超えてしまった場合の処理を作りました。

今回は同じように、右端・上端・下端を超えてしまった場合の処理を追加します。

前回の vgtコードをコピーして、

cp 32_adjust_index.vgt.json 33_adjust_index_2.vgt.json

コメントをちょっと修正して、

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -39,14 +39,14 @@
     , ["set", "yt", ["+", "y", -1]]
     , ["set", "yb", ["+", "y",  1]]
 
-    , ["_cmt", "★ 補正の直前"]
+    , ["_cmt", "★ xl の補正の直前"]
     , ["case"
       , [["eq", "xl", -1]
-        , ["_cmt", "★ -1 だった場合"]
+        , ["_cmt", "★ 左端を超えた場合"]
         , ["set", "xl", ["+", "w", -1]]
         ]
       ]
-    , ["_cmt", "★ 補正の直後"]
+    , ["_cmt", "★ xl の補正の直後"]
 
     , ["var", "tmp"]

まず右端から。

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -48,6 +48,15 @@
       ]
     , ["_cmt", "★ xl の補正の直後"]
 
+    , ["_cmt", "★ xr の補正の直前"]
+    , ["case"
+      , [["eq", "xr", "w"]
+        , ["_cmt", "★ 右端を超えた場合"]
+        , ["set", "xr", 0]
+        ]
+      ]
+    , ["_cmt", "★ xr の補正の直後"]
+
     , ["var", "tmp"]
 
     , ["_cmt", "左上"]
@@ -99,7 +108,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", 0, 2]
+    , ["call", "count_alive", "w", 4, 2]
     ]
   ]

動作の確認のため、注目するセルを (4, 2) として、 xr == 5 だったら xr = 0 に補正されることを期待して、実行。

================================
52: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      142   ["set_reg_a", 1]
      144 ["label", "end_eq_2"]
      146   ["set_reg_b", 1]
      148   ["compare"]
      149   ["jump_eq", 155]
      151   ["jump", 169]
      153 ["label", "when_1_0"]
      155   ["_cmt", "★~左端を超えた場合"]
      157   ["set_reg_a", "[bp+2]"]
      159   ["set_reg_b", -1]
      161   ["add_ab"]
      162   ["cp", "reg_a", "[bp-2]"]
      165   ["jump", 169]
      167 ["label", "end_case_1"]
      169   ["_cmt", "★~xl~の補正の直後"]
pc => 171   ["_cmt", "★~xr~の補正の直前"]
      173   ["set_reg_a", "[bp-3]"]
      175   ["set_reg_b", "[bp+2]"]
      177   ["compare"]
      178   ["jump_eq", 186]
      180   ["set_reg_a", 0]
      182   ["jump", 190]
      184 ["label", "then_4"]
      186   ["set_reg_a", 1]
      188 ["label", "end_eq_4"]
      190   ["set_reg_b", 1]
      192   ["compare"]
      193   ["jump_eq", 199]
      195   ["jump", 208]
      197 ["label", "when_3_0"]
      199   ["_cmt", "★~右端を超えた場合"]
      201   ["cp", 0, "[bp-3]"]
---- memory (stack) ----
         25 0
         26 0
         27 0
         28 0
         29 0
         30 0
         31 0
         32 0
sp    => 33 3
         34 1
         35 5 ... xr
         36 3
         37 0
   bp => 38 47
         39 457
         40 5
         41 4

補正前に 5 になっていた xr が……

(step=62)

      171   ["_cmt", "★~xr~の補正の直前"]
      173   ["set_reg_a", "[bp-3]"]
      175   ["set_reg_b", "[bp+2]"]
      177   ["compare"]
      178   ["jump_eq", 186]
      180   ["set_reg_a", 0]
      182   ["jump", 190]
      184 ["label", "then_4"]
      186   ["set_reg_a", 1]
      188 ["label", "end_eq_4"]
      190   ["set_reg_b", 1]
      192   ["compare"]
      193   ["jump_eq", 199]
      195   ["jump", 208]
      197 ["label", "when_3_0"]
pc => 199   ["_cmt", "★~右端を超えた場合"]
      201   ["cp", 0, "[bp-3]"]
      204   ["jump", 208]
      206 ["label", "end_case_3"]
      208   ["_cmt", "★~xr~の補正の直後"]

「右端を超えた場合」の分岐に入って

================================
65: reg_a(1) reg_b(1) reg_c(0) zf(1)
---- memory (main) ----
      178   ["jump_eq", 186]
      180   ["set_reg_a", 0]
      182   ["jump", 190]
      184 ["label", "then_4"]
      186   ["set_reg_a", 1]
      188 ["label", "end_eq_4"]
      190   ["set_reg_b", 1]
      192   ["compare"]
      193   ["jump_eq", 199]
      195   ["jump", 208]
      197 ["label", "when_3_0"]
      199   ["_cmt", "★~右端を超えた場合"]
      201   ["cp", 0, "[bp-3]"]
      204   ["jump", 208]
      206 ["label", "end_case_3"]
pc => 208   ["_cmt", "★~xr~の補正の直後"]
      210   ["sub_sp", 1]
      212   ["_cmt", "左上"]
      214   ["push", "[bp-4]"]
      216   ["push", "[bp-2]"]
      218   ["push", "[bp+2]"]
      220   ["_cmt", "call_set~~vram_get"]
      222   ["call", 41]
      224   ["add_sp", 3]
      226   ["cp", "reg_a", "[bp-6]"]
      229   ["set_reg_a", "[bp-1]"]
      231   ["set_reg_b", "[bp-6]"]
      233   ["add_ab"]
      234   ["cp", "reg_a", "[bp-1]"]
      237   ["_cmt", "上"]
---- memory (stack) ----
         25 0
         26 0
         27 0
         28 0
         29 0
         30 0
         31 0
         32 0
sp    => 33 3
         34 1
         35 0 ... xr
         36 3
         37 0
   bp => 38 47
         39 457
         40 5
         41 4

0 になりました! いいですね。


同様に、上端を超えた場合の補正を追加:

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -24,7 +24,7 @@
     ]
   ]
 
-, ["func", "count_alive", ["w", "x", "y"]
+, ["func", "count_alive", ["w", "h", "x", "y"]
   , [
       ["var", "count"]
     , ["set", "count", 0]
@@ -57,6 +57,15 @@
       ]
     , ["_cmt", "★ xr の補正の直後"]
 
+    , ["_cmt", "★ yt の補正の直前"]
+    , ["case"
+      , [["eq", "yt", -1]
+        , ["_cmt", "★ 上端を超えた場合"]
+        , ["set", "yt", ["+", "h", -1]]
+        ]
+      ]
+    , ["_cmt", "★ yt の補正の直後"]
+
     , ["var", "tmp"]
 
     , ["_cmt", "左上"]
@@ -108,7 +117,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", 4, 2]
+    , ["call", "count_alive", "w", "h", 2, 0]
     ]
   ]

上端・下端の場合は幅の代わりに高さ h が必要なので count_alive() の引数として渡すようにしました。

右端、左端のときと同じ要領なので動作確認の結果は省略します。

最後に下端。

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -66,6 +66,15 @@
       ]
     , ["_cmt", "★ yt の補正の直後"]
 
+    , ["_cmt", "★ yb の補正の直前"]
+    , ["case"
+      , [["eq", "yb", "h"]
+        , ["_cmt", "★ 下端を超えた場合"]
+        , ["set", "yb", 0]
+        ]
+      ]
+    , ["_cmt", "★ yb の補正の直後"]
+
     , ["var", "tmp"]
 
     , ["_cmt", "左上"]
@@ -117,7 +126,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", "h", 2, 0]
+    , ["call", "count_alive", "w", "h", 2, 4]
     ]
   ]

四隅の各セルに注目している場合も確認しておきましょう。

左上 (0, 0) に注目している場合:

================================
98: reg_a(0) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
(略)
      284   ["cp", 0, "[bp-5]"]
      287   ["jump", 291]
      289 ["label", "end_case_7"]
pc => 291   ["_cmt", "★~yb~の補正の直後"]
      293   ["sub_sp", 1]
      295   ["_cmt", "左上"]
      297   ["push", "[bp-4]"]
(略)
---- memory (stack) ----
         24 0
         25 0
         26 0
         27 0
         28 0
         29 0
         30 0
         31 0
sp    => 32 1 ... yb: 0 + 1            
         33 4 ... yt: 0 - 1 => 4 に補正
         34 1 ... xr: 0 + 1            
         35 4 ... xl: 0 - 1 => 4 に補正
         36 0
   bp => 37 47
         38 542
         39 5
         40 5

期待どおりの動きです。

右上 (4, 0) に注目している場合 (以下、同様なので yb の補正の直後の時点でのスタック領域の一部だけ示します):

(step=95)

sp    => 32 1 ... yb: 0 + 1
         33 4 ... yt: 0 - 1 => 4 に補正
         34 0 ... xr: 4 + 1 => 0 に補正
         35 3 ... xl: 4 - 1
         36 0
   bp => 37 47

左下 (0, 4) に注目している場合:

(step=95)

sp    => 32 0 ... yb: 4 + 1 => 0 に補正
         33 3 ... yt: 4 - 1
         34 1 ... xr: 0 + 1
         35 4 ... xl: 0 - 1 => 4 に補正
         36 0
   bp => 37 47

右下 (4, 4) に注目している場合:

(step=92)

sp    => 32 0 ... yb: 4 + 1 => 0 に補正
         33 3 ... yt: 4 - 1
         34 0 ... xr: 4 + 1 => 0 に補正
         35 3 ... xl: 4 - 1
         36 0
   bp => 37 47

(2, 2) に注目している場合(補正が発生しない場合)も念のため再度確認しておきましょうか。

(step=88)

sp    => 32 3 ... yb: 2 + 1
         33 1 ... yt: 2 - 1
         34 3 ... xr: 2 + 1
         35 1 ... xl: 2 - 1
         36 0
   bp => 37 47

問題ないようです!


さて、 右端、左端、上端、下端のそれぞれについて case文を追加することで補正をしましたが、 似た処理なのでこれは共通化しておくと良さそうです。

関数にしてみました:

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -24,6 +24,27 @@
     ]
   ]
 
+, ["func", "adjust_index", ["width", "i"]
+  , [
+      ["var", "adjusted"]
+    , ["var", "max_i"]
+    , ["set", "max_i", ["+", "width", -1]]
+
+    , ["case"
+      , [["eq", "i", -1]
+        , ["_cmt", "下限を超えた場合"]
+        , ["set", "adjusted", "max_i"]]
+      , [["eq", "i", "width"]
+        , ["_cmt", "上限を超えた場合"]
+        , ["set", "adjusted", 0]]
+      , [["eq", 1, 1]
+        , ["_cmt", "補正が不要な場合"]
+        , ["set", "adjusted", "i"]]
+      ]
+    , ["return", "adjusted"]
+    ]
+  ]
+
 , ["func", "count_alive", ["w", "h", "x", "y"]
   , [
       ["var", "count"]

たとえば xl の場合は「右端を超えたか」のチェックは不要なので、 「下限の補正」「上限の補正」に分けてもいいかなという気もしましたが、 とりあえずはこれでいいんじゃないでしょうか。 気になるようであればライフゲームが動いてから好きなだけいじりましょう。

main() の先頭に適当に確認用コードを追加して、 簡単に動作確認してみます。

, ["func", "main", []
  , [
      ["var", "tmp"]

    , ["call_set", "tmp", ["adjust_index", 5, -1]]
    , ["_cmt", "★ 座標補正の確認 下端: 4 になるべき"]

    , ["call_set", "tmp", ["adjust_index", 5, 5]]
    , ["_cmt", "★ 座標補正の確認 上端: 0 になるべき"]

    , ["call_set", "tmp", ["adjust_index", 5, 1]]
    , ["_cmt", "★ 座標補正の確認 補正なし: 1 になるべき"]

    // 略

期待通り(上記のコメントで書いてある通り)に動いていることを確認できたので、 確認用コードをコメントアウトしてから、 補正処理の箇所を adjust_index() を使うように置き換えていきます。

まずは左端を超えた場合の xl の補正だけ。

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -61,12 +61,7 @@
     , ["set", "yb", ["+", "y",  1]]
 
     , ["_cmt", "★ xl の補正の直前"]
-    , ["case"
-      , [["eq", "xl", -1]
-        , ["_cmt", "★ 左端を超えた場合"]
-        , ["set", "xl", ["+", "w", -1]]
-        ]
-      ]
+    , ["call_set", "xl", ["adjust_index", "w", "xl"]]
     , ["_cmt", "★ xl の補正の直後"]
 
     , ["_cmt", "★ xr の補正の直前"]
@@ -96,6 +91,8 @@
       ]
     , ["_cmt", "★ yb の補正の直後"]
 
+    , ["_cmt", "★ 座標補正の直後"]
+
     , ["var", "tmp"]
 
     , ["_cmt", "左上"]
@@ -160,7 +157,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", "h", 2, 4]
+    , ["call", "count_alive", "w", "h", 0, 2]
     ]
   ]

ついでに、4つの補正が全部終わったところに ★ 座標補正の直後 というコメントも追加しました。

================================
72: reg_a(4) reg_b(1) reg_c(0) zf(1)
---- memory (main) ----
      238   ["set_reg_b", -1]
      240   ["add_ab"]
      241   ["cp", "reg_a", "[bp-4]"]
      244   ["set_reg_a", "[bp+5]"]
      246   ["set_reg_b", 1]
      248   ["add_ab"]
      249   ["cp", "reg_a", "[bp-5]"]
      252   ["_cmt", "★~xl~の補正の直前"]
      254   ["push", "[bp-2]"]
      256   ["push", "[bp+2]"]
      258   ["_cmt", "call_set~~adjust_index"]
      260   ["call", 77]
      262   ["add_sp", 2]
      264   ["cp", "reg_a", "[bp-2]"]
pc => 267   ["_cmt", "★~xl~の補正の直後"]
      269   ["_cmt", "★~xr~の補正の直前"]
      271   ["set_reg_a", "[bp-3]"]
      273   ["set_reg_b", "[bp+2]"]
      275   ["compare"]
      276   ["jump_eq", 284]
      278   ["set_reg_a", 0]
      280   ["jump", 288]
      282 ["label", "then_6"]
      284   ["set_reg_a", 1]
      286 ["label", "end_eq_6"]
      288   ["set_reg_b", 1]
      290   ["compare"]
      291   ["jump_eq", 297]
      293   ["jump", 306]
      295 ["label", "when_5_0"]
      297   ["_cmt", "★~右端を超えた場合"]
---- memory (stack) ----
         24 0
         25 0
         26 4
         27 4
         28 37
         29 262
         30 5
         31 -1
sp    => 32 3
         33 1
         34 1
         35 4 ... xl
         36 0
   bp => 37 47
         38 642
         39 5
         40 5

問題ないようです。 xl 以外も同じように動作確認しつつ置き換えていきましょう。

--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -65,12 +65,7 @@
     , ["_cmt", "★ xl の補正の直後"]
 
     , ["_cmt", "★ xr の補正の直前"]
-    , ["case"
-      , [["eq", "xr", "w"]
-        , ["_cmt", "★ 右端を超えた場合"]
-        , ["set", "xr", 0]
-        ]
-      ]
+    , ["call_set", "xr", ["adjust_index", "w", "xr"]]
     , ["_cmt", "★ xr の補正の直後"]
 
     , ["_cmt", "★ yt の補正の直前"]
@@ -157,7 +152,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", "h", 0, 2]
+    , ["call", "count_alive", "w", "h", 4, 2]
     ]
   ]
--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -69,12 +69,7 @@
     , ["_cmt", "★ xr の補正の直後"]
 
     , ["_cmt", "★ yt の補正の直前"]
-    , ["case"
-      , [["eq", "yt", -1]
-        , ["_cmt", "★ 上端を超えた場合"]
-        , ["set", "yt", ["+", "h", -1]]
-        ]
-      ]
+    , ["call_set", "yt", ["adjust_index", "h", "yt"]]
     , ["_cmt", "★ yt の補正の直後"]
 
     , ["_cmt", "★ yb の補正の直前"]
@@ -152,7 +147,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", "h", 4, 2]
+    , ["call", "count_alive", "w", "h", 2, 0]
     ]
   ]
--- a/33_adjust_index_2.vgt.json
+++ b/33_adjust_index_2.vgt.json
@@ -73,12 +73,7 @@
     , ["_cmt", "★ yt の補正の直後"]
 
     , ["_cmt", "★ yb の補正の直前"]
-    , ["case"
-      , [["eq", "yb", "h"]
-        , ["_cmt", "★ 下端を超えた場合"]
-        , ["set", "yb", 0]
-        ]
-      ]
+    , ["call_set", "yb", ["adjust_index", "h", "yb"]]
     , ["_cmt", "★ yb の補正の直後"]
 
     , ["_cmt", "★ 座標補正の直後"]
@@ -147,7 +142,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["call", "count_alive", "w", "h", 2, 0]
+    , ["call", "count_alive", "w", "h", 2, 4]
     ]
   ]

というわけで、座標補正処理、完成です!

vm2gol v2 製作メモ(32) 座標の補正


これまでは、 その都度の目標に向けて適当に作り、バグや not yet impl で例外が出たら その都度潰していく、という進め方でやってきました。

そういう方針なので、それはそれでいいのですが、 同じようなことの繰り返しになってちょっと煩雑&退屈なので、 特に瑣末なものは先に潰してから本題に入ろうと思います。 なるべくライフゲームに専念できるように。

(これはブログ記事化する都合上先回りしているためで、 そういう都合がなければ先回りしなくていいと思います)


というわけで、まずは codegen_set() でローカル変数が解決できていなかった箇所を修正。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -266,6 +266,8 @@ def codegen_set(fn_arg_names, lvar_names, rest)
       "reg_a"
     when fn_arg_names.include?(rest[1])
       to_fn_arg_addr(fn_arg_names, rest[1])
+    when lvar_names.include?(rest[1])
+      to_lvar_addr(lvar_names, rest[1])
     when /^vram\[(.+)\]$/ =~ rest[1]
       vram_addr = $1
       alines << "  get_vram #{vram_addr} reg_a"

それから、 codegen_stmts()_cmt に対応していなかったのを修正:

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -397,6 +397,8 @@ def codegen_stmts(fn_arg_names, lvar_names, rest)
       alines += codegen_set(fn_arg_names, lvar_names, 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

はい、ではライフゲームに戻りましょう。

まずは前回のコードをコピーして、と。

cp 31_count_alive_2.vgt.json 32_adjust_index.vgt.json

前回は8つのセルを使った生存カウントができるところまで作りましたが、 まだ完全ではありません。

盤面の上の辺と下の辺、 左の辺と右の辺をつなげてトーラス(※1)にする必要があります。

※1 トーラス - Wikipedia
https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%BC%E3%83%A9%E3%82%B9


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

今の vgt コードはこうなっています(座標補正なしの状態)。

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

この部分を書き換えていきます。

まずは xl に着目して左端の処理だけをやってみましょう。

--- a/32_adjust_index.vgt.json
+++ b/32_adjust_index.vgt.json
@@ -39,6 +39,15 @@
     , ["set", "yt", ["+", "y", -1]]
     , ["set", "yb", ["+", "y",  1]]
 
+    , ["_cmt", "★ 補正の直前"]
+    , ["case"
+      , [["eq", "xl", -1]
+        , ["_cmt", "★ -1 だった場合"]
+        , ["set", "xl", ["+", "w", -1]]
+        ]
+      ]
+    , ["_cmt", "★ 補正の直後"]
+
     , ["var", "tmp"]
 
     , ["_cmt", "左上"]

一度 xl = x - 1 を実行してから、 xl == -1 だったら xl = w - 1 で代入しなおす、というやり方にしました。

生存セルの初期化処理はいったん消して、 注目するセルを (0, 2) にして動かしてみましょう。

--- a/32_adjust_index.vgt.json
+++ b/32_adjust_index.vgt.json
@@ -99,18 +99,7 @@
     , ["var", "y"]
     , ["set", "y", 0]
 
-    , ["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]
+    , ["call", "count_alive", "w", 0, 2]
     ]
   ]

とりあえず実行:

$ ./run.sh 32_adjust_index.vgt.json 
vgvm.rb:365:in `num_args_for': Invalid operator (-1) (RuntimeError)
    from vgvm.rb:31:in `dump_main'
    from vgvm.rb:382:in `dump_v2'
    from vgvm.rb:149:in `start'
    from vgvm.rb:470:in `<main>'

ふーむ。num_args_for() でこけましたね。なんじゃこりゃ?

いきなり終了してしまうためメモリのダンプ表示が見れませんが、 こういう場合は落ち着いてアセンブリコード(または機械語コード)を確認します。

cat -n tmp/32_adjust_index.vga.txt の出力の一部:

    73    _cmt ★~補正の直前
    74    # 条件 1_0: ["eq", "xl", -1]
    75    set_reg_a xl
    76    set_reg_b -1
    77    compare
    78    jump_eq when_1_0
    79    jump end_case_1
    80  label when_1_0
    81    _cmt ★ -1 だった場合
    82    set xl + w -1  # ●
    83    jump end_case_1
    84  label end_case_1
    85    _cmt ★~補正の直後

● のとこがなんか無茶なことになってますね……。

codegen_case() のここです。

    cond, *rest = when_block

    # ...

      then_alines = ["label when_#{label_id}_#{when_idx}"]
      rest.each {|stmt|
        then_alines << "  " + stmt.join(" ")
      }

あーこれはだめですね。 これを書いたときはやっつけで済ませて、動いたのでよし、 としていましたが、 やっつけでは済まない事態が訪れました。ちゃんとやりましょう。

ちゃんとやるといっても、 ここでやっているのは「文の連なり」の変換(コード生成)なので、 codegen_stmts() に置き換えるだけでいいはず。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -18,7 +18,7 @@ def to_lvar_addr(lvar_names, lvar_name)
   "[bp-#{index + 1}]"
 end
 
-def codegen_case(when_blocks)
+def codegen_case(fn_arg_names, lvar_names, when_blocks)
   alines = []
   $label_id += 1
   label_id = $label_id
@@ -40,9 +40,7 @@ def codegen_case(when_blocks)
       alines << "  jump_eq when_#{label_id}_#{when_idx}"
 
       then_alines = ["label when_#{label_id}_#{when_idx}"]
-      rest.each {|stmt|
-        then_alines << "  " + stmt.join(" ")
-      }
+      then_alines += codegen_stmts(fn_arg_names, lvar_names, rest)
       then_alines << "  jump end_case_#{label_id}"
       then_bodies << then_alines
     else
@@ -365,7 +363,7 @@ def codegen_func_def(rest)
     when "return"
       alines += codegen_return(lvar_names, stmt_rest)
     when "case"
-      alines += codegen_case(stmt_rest)
+      alines += codegen_case(fn_arg_names, lvar_names, stmt_rest)
     when "while"
       alines += codegen_while(fn_arg_names, lvar_names, stmt_rest)
     when "_cmt"

動かします。

================================
41: reg_a(3) reg_b(1) reg_c(0) zf(0)
---- memory (main) ----
      99   ["add_ab"]
      100   ["cp", "reg_a", "[bp-2]"]
      103   ["set_reg_a", "[bp+3]"]
      105   ["set_reg_b", 1]
      107   ["add_ab"]
      108   ["cp", "reg_a", "[bp-3]"]
      111   ["set_reg_a", "[bp+4]"]
      113   ["set_reg_b", -1]
      115   ["add_ab"]
      116   ["cp", "reg_a", "[bp-4]"]
      119   ["set_reg_a", "[bp+4]"]
      121   ["set_reg_b", 1]
      123   ["add_ab"]
      124   ["cp", "reg_a", "[bp-5]"]
      127   ["_cmt", "★~補正の直前"]
pc => 129   ["set_reg_a", "xl"]
      131   ["set_reg_b", -1]
      133   ["compare"]
      134   ["jump_eq", 140]
      136   ["jump", 154]
      138 ["label", "when_1_0"]
      140   ["_cmt", "★~-1~だった場合"]
      142   ["set_reg_a", "[bp+2]"]
      144   ["set_reg_b", -1]
      146   ["add_ab"]
      147   ["cp", "reg_a", "[bp-2]"]
      150   ["jump", 154]
      152 ["label", "end_case_1"]
      154   ["_cmt", "★~補正の直後"]
      156   ["sub_sp", 1]
      158   ["_cmt", "左上"]
---- memory (stack) ----
         25 0
         26 0
         27 0
         28 0
         29 0
         30 0
         31 0
         32 0
sp    => 33 3
         34 1
         35 1
         36 -1
         37 0
   bp => 38 47
         39 403
         40 5
         41 0
---- memory (vram) ----
..... .....
..... .....
..... .....
..... .....
..... .....
vgvm.rb:430:in `set_reg_a': Not yet implemented ("val") ("xl") (RuntimeError)
    from vgvm.rb:166:in `block in start'
    from vgvm.rb:152:in `loop'
    from vgvm.rb:152:in `start'
    from vgvm.rb:470:in `<main>'

補正処理の一番最初での参照が解決されずに xl が残っているな、 と当たりを付けて、このアセンブリコードを生成している箇所を探します。

codegen_case() ですね。ここ:

def codegen_case(fn_arg_names, lvar_names, when_blocks)

    # ...

    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_#{label_id}_#{when_idx}"

      then_alines = ["label when_#{label_id}_#{when_idx}"]
      then_alines += codegen_stmts(fn_arg_names, lvar_names, rest)
      then_alines << "  jump end_case_#{label_id}"
      then_bodies << then_alines
    else
      # ...

なんだか codegen_case() がやっつけすぎな気がしますが、いいんですこれで。 YAGNI だから。 今が直すタイミングだからこれでいいの!! (ということにして……)

上のコードの ★ のとこでローカル変数の参照が解決できてないわけで、 また同じパターンだなーと言って、これまでのように書き換えて解決してもいいのですが、 実はここは横着できます。

ここでやっているのは「式のコンパイル」なのですが、 そのためのメソッド codegen_exp() がもうすでにあります。 これを使い回せないでしょうか? codegen_exp() の方ではローカル変数の解決も実装済みです。

codegen_exp() では、 オペレータが eq の場合は、 2つのオペランドが同じ値だったら 1 を、 異なっていたら 0 を reg_a にセットします。

なので、まず codegen_exp() を実行して、 その結果を見て分岐するようにすればいいんじゃないでしょうか?

やってみます。

--- a/vgcg.rb
+++ b/vgcg.rb
@@ -34,8 +34,12 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
 
     case cond_head
     when "eq"
-      alines << "  set_reg_a #{cond_rest[0]}"
-      alines << "  set_reg_b #{cond_rest[1]}"
+      # 式の結果が reg_a に入る
+      alines += codegen_exp(fn_arg_names, lvar_names, cond)
+
+      # 式の結果と比較するための値を reg_b に入れる
+      alines << "  set_reg_b 1"
+
       alines << "  compare"
       alines << "  jump_eq when_#{label_id}_#{when_idx}"

実行!

$ ./run.sh 32_adjust_index.vgt.json 
# 略
================================
50: reg_a(1) reg_b(1) reg_c(0) zf(1)
---- memory (main) ----
      127   ["_cmt", "★~補正の直前"]
      129   ["set_reg_a", "[bp-2]"]
      131   ["set_reg_b", -1]
      133   ["compare"]
      134   ["jump_eq", 142]
      136   ["set_reg_a", 0]
      138   ["jump", 146]
      140 ["label", "then_2"]
      142   ["set_reg_a", 1]
      144 ["label", "end_eq_2"]
      146   ["set_reg_b", 1]
      148   ["compare"]
      149   ["jump_eq", 155]
      151   ["jump", 169]
      153 ["label", "when_1_0"]
pc => 155   ["_cmt", "★~-1~だった場合"]
      157   ["set_reg_a", "[bp+2]"]
      159   ["set_reg_b", -1]
      161   ["add_ab"]
      162   ["cp", "reg_a", "[bp-2]"]
      165   ["jump", 169]
      167 ["label", "end_case_1"]
      169   ["_cmt", "★~補正の直後"]
      171   ["sub_sp", 1]
      173   ["_cmt", "左上"]
      175   ["push", "[bp-4]"]
      177   ["push", "[bp-2]"]
      179   ["push", "[bp+2]"]
      181   ["_cmt", "call_set~~vram_get"]
      183   ["call", 41]
      185   ["add_sp", 3]

「-1 だった場合」の分岐に入ってます。よしよし。

================================
56: reg_a(4) reg_b(-1) reg_c(0) zf(1)
---- memory (main) ----
      140 ["label", "then_2"]
      142   ["set_reg_a", 1]
      144 ["label", "end_eq_2"]
      146   ["set_reg_b", 1]
      148   ["compare"]
      149   ["jump_eq", 155]
      151   ["jump", 169]
      153 ["label", "when_1_0"]
      155   ["_cmt", "★~-1~だった場合"]
      157   ["set_reg_a", "[bp+2]"]
      159   ["set_reg_b", -1]
      161   ["add_ab"]
      162   ["cp", "reg_a", "[bp-2]"]
      165   ["jump", 169]
      167 ["label", "end_case_1"]
pc => 169   ["_cmt", "★~補正の直後"]
      171   ["sub_sp", 1]
      173   ["_cmt", "左上"]
      175   ["push", "[bp-4]"]
      177   ["push", "[bp-2]"]
      179   ["push", "[bp+2]"]
      181   ["_cmt", "call_set~~vram_get"]
      183   ["call", 41]
      185   ["add_sp", 3]
      187   ["cp", "reg_a", "[bp-6]"]
      190   ["set_reg_a", "[bp-1]"]
      192   ["set_reg_b", "[bp-6]"]
      194   ["add_ab"]
      195   ["cp", "reg_a", "[bp-1]"]
      198   ["_cmt", "上"]
---- memory (stack) ----
         25 0
         26 0
         27 0
         28 0
         29 0
         30 0
         31 0
         32 0
sp    => 33 3
         34 1
         35 1
         36 4 ... xl
         37 0
   bp => 38 47
         39 418
         40 5
         41 0

補正の直後では xl == 4 になっています! ヨシ!

今回はここまで!

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) ----
..... .....
.@@.. .....
.@.@. .....
..@@. .....
..... .....

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

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

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