- 目次ページに戻る / 前 / 次
- 前回からの差分をまとめて見る
関数まわりと条件分岐とかが一応できて、入れ子の式も回避方法が決まり…… たぶんまだ足りないところがボロボロ出てくると思いますが、 ライフゲームを作りながら直していきましょう。
まずはライフゲームのロジックの確認のため Ruby で書いてみます。
# coding: utf-8 $w = 8 $h = 6 $grid = [] $buf = [] # init (0...$h).each {|y| $grid[y] = [] $buf[y] = [] } (0...$h).each {|y| (0...$w).each {|x| puts "#{x} #{y}" $grid[y][x] = 0 $buf[y][x] = 0 } } def dump (0...$h).each {|y| puts $grid[y].map {|v| v == 0 ? " " : "@" }.join("") } puts "--------" end $grid[0][0] = 1 $grid[0][1] = 1 $grid[0][2] = 1 $grid[1][2] = 1 $grid[2][1] = 1 loop do (0...$h).each {|y| (0...$w).each {|x| 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] 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 } } (0...$h).each {|y| (0...$w).each {|x| $grid[y][x] = $buf[y][x] } } dump sleep 0.1 end
あんまり凝らないようにしてベタに書いてみました。 こんなもんでいいでしょう。 これを vgt のコードに書き換えていきます。
と、これを眺めていると大事なものがまだ用意できていないことに気づきますね? (またこのパターンか〜)
そう、配列です。 これもまじめにやろうとするとヒープ領域を用意して……となる気がします。 ちょっと面倒そうです。
それと、未解決というか決めていなかった問題としてもうひとつ、 画面への出力をどうするかというのがあります。 難しいことはせずに単に端末にプリントするだけでいいでしょ、 程度にぼんやり考えてましたが、さて。 ていうかプリント? コンピュータにおけるプリントとは?
どうしようかなと悩みましたが、 メインメモリ、スタック領域とは別に配列専用の領域を用意して VRAM と兼用することにしました。
ここでもまた我々は初心者なので(難しいことはしてはいけない云々)、というわけです。 締め切りも迫ってるし難しいことやってる余裕はないのです…… とにかくライフゲームが動けばそれでええんや!(とまた言い聞かせる)
※ 「VRAM といったら普通こういうものだ」という知識がないので、 VRAM と呼んではいますがなんか間違ってるかもしれません。 配列と兼用なのも強引な気はしています。自覚はあるのです……。 あとで調べるのです……あとで……。
はい、では VRAM兼配列を用意しましょう。
サイズは固定で、まずは試しということで小さい領域を用意します (あとで大きくします)。
--- a/vgvm.rb +++ b/vgvm.rb @@ -10,13 +10,15 @@ module TermColor end class Memory - attr_accessor :main, :stack + attr_accessor :main, :stack, :vram def initialize(stack_size) @main = [] # スタック領域 @stack = Array.new(stack_size, 0) + + @vram = Array.new(10, 0) end def dump_main(pc)
で、0 はじまりのインデックス (VRAMのアドレス)を指定して値のセットと値の取得ができるようにしましょう。
この2つの VM命令を追加します:
set_vram {VRAMのアドレス} {セットする値} get_vram {VRAMのアドレス} {取得した値の格納先}
修正します。
VM命令 set_vram
を追加:
--- a/vgvm.rb +++ b/vgvm.rb @@ -209,6 +209,13 @@ class Vm end set_sp(@sp + 1) @pc += pc_delta + when "set_vram" + arg1 = @mem.main[@pc + 1] + arg2 = @mem.main[@pc + 2] + + @mem.vram[arg1] = arg2 + + @pc += pc_delta else raise "Unknown operator (#{op})" end @@ -255,7 +262,7 @@ class Vm def self.num_args_for(operator) case operator - when "cp" + when "cp", "set_vram" 2 when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp", "sub_sp", "jump_eq", "jump" 1
VM命令 get_vram
を追加:
--- a/vgvm.rb +++ b/vgvm.rb @@ -216,6 +216,20 @@ class Vm @mem.vram[arg1] = arg2 @pc += pc_delta + when "get_vram" + arg1 = @mem.main[@pc + 1] + arg2 = @mem.main[@pc + 2] + + val = @mem.vram[arg1] + + case arg2 + when "reg_a" + @reg_a = val + else + raise not_yet_impl("arg2", arg2) + end + + @pc += pc_delta else raise "Unknown operator (#{op})" end @@ -262,7 +276,7 @@ class Vm def self.num_args_for(operator) case operator - when "cp", "set_vram" + 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" 1
(
※ get_vram
で取得した値をどこに格納するか分岐するように書いていますが、結局 reg_a
以外にセットすることはありませんでした。
)
メインメモリやスタック領域と同じく、 ただの Ruby の配列なので文字列でもオブジェクトでも何でも入れ放題ですが、 ライフゲームのセルごとの生死を入れておくだけなので 数字の 0 または 1 だけを入れるように決めておきます。 0 が死、 1 が生とします。
それから、このままだと中で何が起こってるかが分からないので ダンプ表示に VRAM のダンプを追加します。 まだサイズが小さいですし、まずはお手軽に inspect しとけばいいでしょう。
--- a/vgvm.rb +++ b/vgvm.rb @@ -90,6 +90,10 @@ class Memory end lines.join("\n") end + + def dump_vram + @vram.inspect + end end class Vm @@ -303,6 +307,8 @@ class Vm #{ @mem.dump_main(@pc) } ---- memory (stack) ---- #{ @mem.dump_stack(@sp, @bp) } +---- memory (vram) ---- +#{ @mem.dump_vram() } EOB end
はい。では vgt のコードを書いて動作確認です。 コード生成器の方を修正してないのでまだ動きませんが、 まずはイメージということで。
// 25_vram_set_get.vgt.json ["stmts" , ["func", "main", [] , [ ["var", "x"] , ["set", "vram[0]", 1] , ["set", "x", "vram[0]"] , ["set", "vram[0]", 0] , ["set", "x", "vram[0]"] , ["set", "vram[1]", 1] , ["set", "x", "vram[1]"] ] ] ]
コード生成器のレイヤーでは、 set
を拡張する形で対応させることにします。
( ここらへんになってくると 「そういえば set文ってどうしてたっけ……これちゃんと動くのかな?」 みたいに、なんというか制御しきれてない部分が出てきて、 ちょっと楽しいですね? )
修正します。
codegen_set()
で VRAM からの値の取得と、VRAM への値のセットができるようにします。
--- a/vgcg.rb +++ b/vgcg.rb @@ -156,12 +156,22 @@ def codegen_set(fn_arg_names, lvar_names, rest) when fn_arg_names.include?(rest[1]) fn_arg_pos = fn_arg_names.index(rest[1]) + 2 "[bp+#{fn_arg_pos}]" + when /^vram\[(.+)\]$/ =~ rest[1] + vram_addr = $1 + alines << " get_vram #{vram_addr} reg_a" + "reg_a" else raise not_yet_impl("set src_val", rest) end + case lvar_name + when /^vram\[(.+)\]$/ + vram_addr = $1 + alines << " set_vram #{vram_addr} #{src_val}" + else lvar_pos = lvar_names.index(lvar_name) + 1 alines << " cp #{src_val} [bp-#{lvar_pos}]" + end alines end
差分を見やすくするためにインデントを元のままにしています。 この後別コミットでインデントだけ修正しました。
あと、 lvar_name
という変数名が実態にそぐわなくなったため
dest
という名前にリネームしておきました。
では動かしましょう!
================================ reg_a(1) reg_b(0) reg_c(0) zf(0) ---- memory (main) ---- 00 ["call", 5] 02 ["exit"] 03 ["label", "main"] 05 ["push", "bp"] 07 ["cp", "sp", "bp"] 10 ["sub_sp", 1] 12 ["set_vram", 0, 1] 15 ["get_vram", 0, "reg_a"] 18 ["cp", "reg_a", "[bp-1]"] pc => 21 ["set_vram", 0, 0] 24 ["get_vram", 0, "reg_a"] 27 ["cp", "reg_a", "[bp-1]"] 30 ["set_vram", 1, 1] 33 ["get_vram", 1, "reg_a"] 36 ["cp", "reg_a", "[bp-1]"] 39 ["cp", "bp", "sp"] 42 ["pop", "bp"] 44 ["ret"] ---- memory (stack) ---- 38 0 39 0 40 0 41 0 42 0 43 0 44 0 45 0 sp => 46 1 ... vram[0] から取り出した値がローカル変数 x にコピーされた bp => 47 49 48 2 49 0 ---- memory (vram) ---- [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
vram[0]
に 1 がセットされ、
その値が reg_a
を経由してローカル変数 x
にコピーされた直後です。
続きを動かすと、
vram[0]
を 0 に戻し、同様に vram[1]
に 1 をセットして……というのが確認できます。
良さそうですね!
追記 2019-12-25
そう、配列です。 これもまじめにやろうとするとヒープ領域を用意して……となる気がします。 ちょっと面倒そうです。
などと適当に書きましたが、 低レイヤを知りたい人のためのCコンパイラ作成入門 には配列もスタックに置く方式のことが書かれていました(ステップ21: 配列を実装する)。 なるほどー。