vm2gol v2 (56) VRAMの読み書きを組み込み関数化



VRAM の読み書きは、配列アクセスのような見た目で書けるようにしていました。

set vram[0] = 1;
set vram_value = vram[0];

これをどうコンパイルしていたか。

レキサでは vram[0] をまるごと1つのトークン、1つの識別子として切り出していました。

まじめにやるなら vram, [, 0, ] のように4つのトークンに分けるところだと思うんですが、それを1つのトークンにまとめていてちょっとズルい雰囲気がありますね。最初はどうやればよいか分かっていなくて手探りでしたし、簡単に済ませるためにこれはこれで良かったとは思いますが。

後段のコード生成処理では識別子が vram[...] のパターンになっているか判別して、 マッチしていたら ... の部分を取り出し、 その部分がローカル変数だったら……というややこしいことをしていました。 VRAM まわりだけ例外扱いになっていて、それで実装が無駄に膨らんでいて野暮ったい。 移植するときもコード生成器の VRAM まわりの部分書くのめんどくさいんですよね。


というわけで今回 vram[...] 記法をまともにサポートする……のではなく、 vram[...] 記法を廃止し、組み込み関数にしてしまいました。

上記の例でいうと次のように書き方が変わります。

call set_vram(0, 1);
call_set vram_value = get_vram(0);

変更後のコードで呼び出している get_vram , set_vram という関数はどこにあるのか?

これはコード生成時に固定のコードを出力して、それを呼び出すようにしています。

def codegen_builtin_set_vram
  puts ""
  puts "label set_vram"
  puts "  push bp"
  puts "  cp sp bp"

  puts "  set_vram [bp:2] [bp:3]" # vram_addr value

  puts "  cp bp sp"
  puts "  pop bp"
  puts "  ret"
end

def codegen_builtin_get_vram
  puts ""
  puts "label get_vram"
  puts "  push bp"
  puts "  cp sp bp"

  puts "  get_vram [bp:2] reg_a" # vram_addr dest

  puts "  cp bp sp"
  puts "  pop bp"
  puts "  ret"
end

def codegen(tree)
  puts "  call main"
  puts "  exit"

  head, *top_stmts = tree
  codegen_top_stmts(top_stmts)

  codegen_builtin_set_vram()
  codegen_builtin_get_vram()
end

呼び出し規約に従ってさえいれば、アセンブリで書かれていても呼び出し側からは普通の関数と同じインターフェイスで使えるので、これでいいわけですね。

先行して v3 で試してみて、悪くなかったので v2 にフィードバックしました。


たとえばC言語なんかで普通にプログラムを作る場合、標準ライブラリが別のオブジェクトファイルとして存在していて、自分の書いたプログラム(のオブジェクトファイル)とリンカでくっつける……みたいな流れですよね。たしか。

それを踏まえて「標準ライブラリの処理をどこに記述し、どこで自分の作ったプログラムとくっつけるか」という視点で考えてみると、 今回の修正では標準ライブラリが(アセンブリコードの形で)コード生成器の中に埋め込まれ、コード生成器が(アセンブリコードを結合することで)リンク相当の処理をする形になったわけですね。 ふむ……。

リンカごっこもそのうちやってみたい。


VRAM のアクセスも通常の関数を処理するルートに乗せたことで 特別扱いしていた部分をなくすことができました。

--- a/vglexer.rb
+++ b/vglexer.rb
@@ -33,7 +33,7 @@ def tokenize(src)
       str = $1
       tokens << Token.new(:sym, str)
       pos += str.size
-    when /\A([a-z_][a-z0-9_\[\]]*)/
+    when /\A([a-z_][a-z0-9_]*)/
       str = $1
       tokens << Token.new(:ident, str)
       pos += str.size
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -204,15 +204,6 @@ def codegen_expr(fn_arg_names, lvar_names, expr)
     when lvar_names.include?(expr)
       cp_src = to_lvar_addr(lvar_names, expr)
       puts "  cp #{cp_src} reg_a"
-    when _match_vram_ref(expr)
-      var_name = _match_vram_ref(expr)
-      case
-      when lvar_names.include?(var_name)
-        vram_addr = to_lvar_addr(lvar_names, var_name)
-        puts "  get_vram #{vram_addr} reg_a"
-      else
-        raise not_yet_impl("rest", rest)
-      end
     else
       raise not_yet_impl("expr", expr)
     end
@@ -245,20 +236,6 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
   puts "  cp reg_a #{lvar_addr}"
 end
 
-def _match_vram_addr(str)
-  md = /^vram\[(\d+)\]$/.match(str)
-  return nil if md.nil?
-
-  md[1]
-end
-
-def _match_vram_ref(str)
-  md = /^vram\[([a-z_][a-z0-9_]*)\]$/.match(str)
-  return nil if md.nil?
-
-  md[1]
-end
-
 def codegen_set(fn_arg_names, lvar_names, rest)
   dest = rest[0]
   expr = rest[1]
@@ -267,18 +244,6 @@ def codegen_set(fn_arg_names, lvar_names, rest)
   src_val = "reg_a"
 
   case
-  when _match_vram_addr(dest)
-    vram_addr = _match_vram_addr(dest)
-    puts "  set_vram #{vram_addr} #{src_val}"
-  when _match_vram_ref(dest)
-    vram_addr = _match_vram_ref(dest)
-    case
-    when lvar_names.include?(vram_addr)
-      lvar_addr = to_lvar_addr(lvar_names, vram_addr)
-      puts "  set_vram #{lvar_addr} #{src_val}"
-    else
-      raise not_yet_impl("dest", dest)
-    end
   when lvar_names.include?(dest)
     lvar_addr = to_lvar_addr(lvar_names, dest)
     puts "  cp #{src_val} #{lvar_addr}"

だいぶさっぱりしました。

その他