Open JTalkのDockerイメージを作って手軽に喋らせる

Docker イメージとラッパースクリプトを作っておいて echo "ほげ" | ./talk.sh みたいな感じで手軽に喋らせたい。

改善の余地はありますがとりあえず動いた段階での最低限なメモです。


Dockerfile 書く:

FROM ubuntu:18.04

RUN apt-get update \
  && apt-get -y install --no-install-recommends \
    hts-voice-nitech-jp-atr503-m001 \
    open-jtalk \
    open-jtalk-mecab-naist-jdic \
    sox \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /root/work

sox は必須ではないが音量調節のためにどうせ使うので一緒に入れておいた。


イメージをビルド:

docker build -t my-open-jtalk:0.0.1 .

コンテナ内で実行させるスクリプト container_open_jtalk.sh を用意:

open_jtalk \
  -x /var/lib/mecab/dic/open-jtalk/naist-jdic \
  -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice \
  -ow /tmp/output.wav

cat /tmp/output.wav

同じくコンテナ内で実行させるスクリプト container_sox.sh を用意:

cat > /tmp/input.wav

# 音量調節
sox -v 0.5 /tmp/input.wav /tmp/output.wav

cat /tmp/output.wav

sox の入出力をパイプにつなげる方法を調べかけたけど、 めんどくさくなったのでひとまず cat と一時ファイルで wrap。。。


コマンドとして使う talk.sh を用意:

#!/bin/bash

gen_wav(){
  docker run --rm -i --name my-open-jtalk \
      -v $(pwd):/root/work \
      my-open-jtalk:0.0.1 \
      /bin/bash /root/work/container_open_jtalk.sh
}

adjust_vol(){
  docker run --rm -i --name my-open-jtalk-sox \
      -v $(pwd):/root/work \
      my-open-jtalk:0.0.1 \
      /bin/bash /root/work/container_sox.sh
}

# 入力は標準入力から受け取る
gen_wav \
  | adjust_vol \
  | aplay --quiet

aplay はホスト側のもの(※ ちなみにホストも Ubuntu)。 別のディレクトリから実行すると pwd がずれて動かないので必要なら適宜なんとかする。


実行:

chmod u+x talk.sh
echo "ほげ" | ./talk.sh
date "+現在の時刻は %-H時 %-M分 です" | ./talk.sh

mei の音声データを使う場合

Dockerfile をこうして

FROM ubuntu:18.04

RUN apt-get ...(略。上記のに加えて wget, unzip もインストールする)

WORKDIR /tmp

RUN wget --no-check-certificate \
      https://sourceforge.net/projects/mmdagent/files/MMDAgent_Example/MMDAgent_Example-1.7/MMDAgent_Example-1.7.zip \
  && unzip MMDAgent_Example-1.7.zip \
  && cp -r MMDAgent_Example-1.7/Voice/mei/ /usr/share/hts-voice/

WORKDIR /root/work

open_jtalk の -m オプションで /usr/share/hts-voice/mei/mei_normal.htsvoice を指定するのじゃ。

参考

vm2gol v2 製作メモ(38) おまけの修正



ゴール設定が「ライフゲームが動けばOK」 だったので、前回で終わり、としてもよかったんですが、 せっかくなのでもうちょっとだけいじります。 すでにライフゲームが動いており目的は達成されているので今回のはおまけです。

第24回 で触れた入れ子の式の問題については別記事で書く予定

ダンプ表示の改良(コメントに色を付ける)

これ v1 のときはやってたんですが、 v2 では単に忘れてた……だった気がします。 忘れていたことを忘れていた? 時間が経つといろんなことを忘れますね。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -7,6 +7,7 @@ require './common'
 module TermColor
   RESET  = "\e[m"
   RED    = "\e[0;31m"
+  BLUE   = "\e[0;34m"
 end
 
 class Memory
@@ -55,6 +56,8 @@ class Memory
         case operator
         when "exit", "call", "ret", "jump", "jump_eq"
           TermColor::RED
+        when "_cmt"
+          TermColor::BLUE
         else
           ""
         end

こんな見た目になります。

f:id:sonota88:20191005110106p:plain

3行だけの修正で見やすくなるので (31) 生存カウント (2) / _cmt の時に入れておくべきでしたね……。

ダンプ表示を間引きして高速化

今の状態だと、グライダーが1周する (右下に移動して左上にワープして、元の位置に戻る) のに4分くらいかかります。

こういう、ターミナルにドバドバ出力するプログラムは ターミナルでの画面表示がボトルネックになる場合が多いので、 単純にダンプ表示を間引きしてやれば速くなる気がします。

動作確認やデバッグのときは1ステップずつ進める必要がありますが、 ライフゲームが動いているのを眺めて愛でる段階では 律儀に毎ステップ表示せずに適当にサボらせればよいでしょう。 とりあえず 「10ステップごとに1回だけダンプ表示を行う」 ようにしてみます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -318,7 +318,7 @@ class Vm
         raise "Unknown operator (#{op})"
       end
 
-      dump_v2()
+      dump_v2() if @step % 10 == 0
       # $stdin.gets if @step >= 600
       # sleep 0.01
     end

やはりこれだけでかなり速くなりますね。 20秒くらいで元の位置に戻るようになりました。

適当に10ステップ毎としましたが、 1周するのに 40万ステップ弱(!)かかるようになっているので、 表示を眺める分にはもっと間引いても問題ありません。

VM: インラインで書いていた命令ごとの処理をメソッドに抽出

Vm#start() が長い。 行数を数えてみると 175行ありました。

case式の中に直接書いている処理をメソッドに抽出しましょう。例として push

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -218,29 +218,7 @@ class Vm
         @pc = ret_addr # 戻る
         set_sp(@sp + 1) # スタックポインタを戻す
       when "push"
-        arg = @mem.main[@pc + 1]
-        val_to_push =
-          case arg
-          when Integer
-            arg
-          when String
-            case arg
-            when "bp"
-              @bp
-            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
-          else
-            raise not_yet_impl("push", arg)
-          end
-        set_sp(@sp - 1)
-        @mem.stack[@sp] = val_to_push
+        push()
         @pc += pc_delta
       when "pop"
         arg = @mem.main[@pc + 1]
@@ -464,6 +442,34 @@ class Vm
       @pc += 2
     end
   end
+
+  def push
+    arg = @mem.main[@pc + 1]
+
+    val_to_push =
+      case arg
+      when Integer
+        arg
+      when String
+        case arg
+        when "bp"
+          @bp
+        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
+      else
+        raise not_yet_impl("push", arg)
+      end
+
+    set_sp(@sp - 1)
+    @mem.stack[@sp] = val_to_push
+  end
 end
 
 exe_file = ARGV[0]

同様に、 pop, set_vram, get_vram もメソッドに抽出しました(diff は省略)。

ライフゲームが動くようになっていますので、 「ライフゲームの動きが(目で見て)おかしくなっていないこと」 をテスト代わりにして、壊れていないことを確認します。 適当だなー。

適当ですが、テストがまだない状態で無理してリファクタリングするよりは、 ちょっと我慢してリファクタリングを後回しにして、 雑でもテストで保護された状態になってから修正する方が安心感がありますね。

codegen_stmts() と codegen_func_def() の共通化

微妙に気にはなっていたところですが。

うーん、これはちょっと考えたんですが、 無理して共通化しなくてもいいかなと。やってもいいけど。 v1 のときはやってたんですけどね。

気が向いたらやるかも。


(2020-08-22 追記) 第46回で修正しました。

使わなくなったメソッドの削除

そういえばそんなのあったな的な。 一番最初に使ったやつみたいですね(忘れてる)。 消しても大丈夫。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -310,22 +310,6 @@ class Vm
     EOB
   end
 
-  def set_mem(addr, n)
-    @mem.main[addr] = n
-  end
-
-  def copy_mem_to_reg_a(addr)
-    @reg_a = @mem.main[addr]
-  end
-
-  def copy_mem_to_reg_b(addr)
-    @reg_b = @mem.main[addr]
-  end
-
-  def copy_reg_c_to_mem(addr)
-    @mem.main[addr] = @reg_c
-  end
-
   def add_ab
     @reg_a = @reg_a + @reg_b
   end

その他メモ

  • なんとなく残したままにしてますが、reg_c は 第5〜7回で使ったあと使わなくなってしまったので、関連箇所を削除してもライフゲームは動きます。

他にもリファクタリングしたり改良したいところはいろいろあるのですが、 だらだらやっているとキリがないのでいったんここでやめておきます。 (気になるところが出てきたらまた追記するかもしれません)

2019-12-14 追記

  • gol.vgt.json: テスト用のコードとコメントアウトしていた部分を一部削除しました


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 で例外が出たら その都度潰していく、という進め方でやってきました。

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

(これはブログ記事化する都合上先回りしているだけで、 そういう都合がなければ先回りしなくていいと思います。 v1 のときは当然やってませんでした)


というわけで、まずは 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) ----
..... .....
.@@.. .....
.@.@. .....
..@@. .....
..... .....

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

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

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

追記 2021-10-03

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

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

f:id:sonota88:20191005110106p:plain



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 でさらに改良してブレークポイントを指定できるようにしました。 ちょっとした修正で済むので、このあたりで先にやっておけばよかったですね。