vm2gol v2 製作メモ(25) 配列とVRAM / set_vram, get_vram


関数まわりと条件分岐とかが一応できて、入れ子の式も回避方法が決まり…… たぶんまだ足りないところがボロボロ出てくると思いますが、 ライフゲームを作りながら直していきましょう。


まずはライフゲームのロジックの確認のため 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: 配列を実装する)。 なるほどー。