- 目次ページに戻る / 前 / 次
- 前回からの差分をまとめて見る
$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
の部分ですが、今度は VM の get_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_a
(vram_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 でさらに改良してブレークポイントを指定できるようにしました。 ちょっとした修正で済むので、このあたりで先にやっておけばよかったですね。