vm2gol v2 製作メモ(7) ラベルとアセンブラ



さて、機械語コードをいくつか書いてきて、 ジャンプ先のアドレスを調べて修正するのに慣れてきた一方で、だんだん面倒にもなってきました。 プログラムを修正するたびに調べるのも面倒だし、書きなおすのも面倒。 こんなの人間のやる仕事じゃねえ!

で、なんか自動化できそうなのでやってみます。

そのためにラベルという命令?   命令というか目印みたいなのを導入します。

ラベルを使って、プログラムをこんな感じで書けないでしょうか?

[
  "set_reg_a", 0,
  "set_reg_b", 0,
  "compare",
  "jump_eq", "then", # ラベルを指定してジャンプ
  "set_reg_c", 3,
  "jump", "endif",   # ラベルを指定してジャンプ
  "label", "then",   # ラベル
  "set_reg_c", 2,
  "label", "endif",  # ラベル
  "set_reg_a", 4,
  "exit"
]

これを変換して、 "then" とか "endif" の部分を数字(アドレス)にすれば、 これまで手で書いてきた機械語コードと同じ位置付けのものになる (VM に渡せるようになる)、という寸法です。

この「変換前のプログラム」のフォーマットも紆余曲折(JSON にしたり)があり、 vm2gol-v1 のときはこんな感じの YAML ファイルに落ち着きました。

- set_reg_a 0
- set_reg_b 0
- compare
- jump_eq then
- set_reg_c 3
- jump endif
- label then
- set_reg_c 2
- label endif
- set_reg_a 4
- exit

ですが、ラベルがインデントされているとやはり見やすいということで、 ちょっと工夫して、たとえばこんなフォーマットを考えてみます。

-   set_reg_a 0
-   set_reg_b 0
-   compare
-   jump_eq then
-   set_reg_c 3
-   jump endif
- label then
-   set_reg_c 2
- label endif
-   set_reg_a 4
-   exit

これを YAML としてパースすると、先頭の余分な空白は無視されるので、 内容的には上のインデントなしと同じになります。

あ、でもこれだと、 このファイルをプログラムで自動生成するときに困りそうかな……。

というわけで、 vm2gol-v2 では YAML はやめてオレオレフォーマットにします。

  set_reg_a 0
  set_reg_b 0 # ここを書き換えて動作確認する
  compare
  jump_eq then
  set_reg_c 3
  jump endif

label then
  set_reg_c 2

label endif
  set_reg_a 4
  exit

これを機械語コードに変換するプログラムを作りました。これです。

# vgasm.rb

# coding: utf-8
require 'pp'
require 'yaml'

def parse(src)
  alines = []
  src.each_line do |line|
    words = line.sub(/#.*/, "").strip.split(/ +/)
    unless words.empty?
      alines << words
    end
  end
  alines
end

def create_label_addr_map(alines)
  map = {}

  addr = 0
  alines.each do |aline|
    head, *rest = aline

    case head
    when "label"
      name = rest[0]
      map[name] = addr
      addr += 2
    else
      addr += 1
      addr += rest.size
    end
  end

  map
end

src = File.read(ARGV[0])
alines = parse(src)

# key: ラベル名、 value: アドレス のマッピングを作る
label_addr_map = create_label_addr_map(alines)
# pp label_addr_map

words = []
alines.each do |aline|
  head, *rest = aline

  words << head

  case head
  when "label"
    words << rest[0]
  when "jump", "jump_eq"
    label_name = rest[0]
    words << label_addr_map[label_name]
  else
    words += rest.map{ |arg|
        (/^\-?\d+$/ =~ arg) ? arg.to_i : arg
      }
  end
end

puts YAML.dump(words)

2パス(2段階)の処理に分かれていて、 まず最初のパスでラベル名とアドレスのマッピングを作り、 2パス目で jumpjump_eq で指定されているラベルをアドレスに置き換えます。

あれっなんかアセンブラができてしまいましたね(わざとらしく)。 そしてこの「変換前のプログラム」はアセンブリのコードですね(わざとらしく)?

拡張子は .vga.txt にします。アセンブリ(Assembly)の a です。

さっきの「変換前のコード」を 07_if.vga.txt に保存して、 実行します!

$ cat 07_if.vga.txt 
  set_reg_a 0
  set_reg_b 0 # ここを書き換えて動作確認する
  compare
  jump_eq then
  set_reg_c 3
  jump endif

label then
  set_reg_c 2

label endif
  set_reg_a 4
  exit

$ ruby vgasm.rb 07_if.vga.txt > 07_if.vge.yaml

$ cat 07_if.vge.yaml
---
- set_reg_a
- 0
- set_reg_b
- 0
- compare
- jump_eq
- 11
- set_reg_c
- 3
- jump
- 15
- label
- then
- set_reg_c
- 2
- label
- endif
- set_reg_a
- 4
- exit

jump, jump_eq の引数がそれぞれ 11, 15 となっていて、 うまく変換できてますね。

これを VM に与えると…

$ ruby vgvm.rb 07_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
pc( 5) | reg_a(0) b(0) c(0) | zf(1)
pc(11) | reg_a(0) b(0) c(0) | zf(1)
vgvm.rb:63:in `block in start': Unknown operator (label) (RuntimeError)
        from vm.rb:28:in `loop'
        from vm.rb:28:in `start'
        from vm.rb:135:in `<main>'

おっと VM の修正を忘れていました。 label の場合は何もする必要がなくて、単に次に進むだけとします。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -53,6 +53,8 @@ class Vm
       when "compare"
         compare()
         @pc += 1
+      when "label"
+        @pc += 2
       when "jump"
         addr = @mem[@pc + 1]
         @pc = addr

では再度実行。

  $ ruby vgvm.rb 07_if.vge.yaml
  pc( 0) | reg_a(0) b(0) c(0) | zf(0)
  pc( 2) | reg_a(0) b(0) c(0) | zf(0)
  pc( 4) | reg_a(0) b(0) c(0) | zf(0)
  pc( 5) | reg_a(0) b(0) c(0) | zf(1) … compare を実行した
  pc(11) | reg_a(0) b(0) c(0) | zf(1) … ラベル "then" の位置にジャンプした
  pc(13) | reg_a(0) b(0) c(0) | zf(1) … label なので何もせず進んだ
  pc(15) | reg_a(0) b(0) c(2) | zf(1) … then句を実行した
  pc(17) | reg_a(0) b(0) c(2) | zf(1) … endif にジャンプした
  pc(19) | reg_a(4) b(0) c(2) | zf(1)
  exit

良さそうですね。

……いや、ちょっと待ってください。 正しい動きではあるんですが、 ラベルそのものがあるアドレスにジャンプしても label の時は何もせず進むだけなので、無駄な感じがしますね。

なので、ジャンプするときはラベルの次の位置にジャンプさせると良いのでは?

やってみましょうか。

@@ -41,7 +41,7 @@ alines.each{ |line|
     words << rest[0]
   when "jump", "jump_eq"
     label_name = rest[0]
-    words << label_addr_map[label_name]
+    words << label_addr_map[label_name] + 2
   else
     words.concat(
       rest.map{ |arg|
$ ruby vgasm.rb 07_if.vga.txt > 07_if.vge.yaml

$ cat 07_if.vge.yaml 
---
- set_reg_a
- 0
- set_reg_b
- 0
- compare
- jump_eq
- 13
- set_reg_c
- 3
- jump
- 17
- label
- then
- set_reg_c
- 2
- label
- endif
- set_reg_a
- 4
- exit

$ ruby vgvm.rb 07_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
pc( 5) | reg_a(0) b(0) c(0) | zf(1)
pc(13) | reg_a(0) b(0) c(0) | zf(1) … ラベルの次の位置にジャンプした
pc(15) | reg_a(0) b(0) c(2) | zf(1)
pc(17) | reg_a(0) b(0) c(2) | zf(1)
pc(19) | reg_a(4) b(0) c(2) | zf(1)
exit

特に問題なさそう。

(こうすると VM に追加した label は用済みな気が…… まあ残したままにしておきましょうか)


なんかあっさりとアセンブラができてしまいました。

といっても命令も値も機械語と同じで、 仕事らしい仕事といえばラベルの変換くらいなので、 アセンブラと言ってしまってよいものかという感じはしますが。

※ ちなみに、この後もアセンブラ部分はほとんど変わりません。これでほぼ完成形。


あ、そうだ、今回のでアセンブルVM での実行の 2段階に分かれて、 実行するのがちょっと面倒になってきたので、 いっぺんに実行させるための簡単な Bash スクリプトを用意しておきます。

#!/bin/bash

set -o errexit

file="$1"
bname=$(basename $file .vga.txt)
exefile=tmp/${bname}.vge.yaml

ruby vgasm.rb $file > $exefile
ruby vgvm.rb $exefile

アセンブラによって生成された .vge.yaml ファイルは tmp/ というディレクトリに入るようにしました。

vm2gol-v1 のときは生成されたファイルも全部 git のリポジトリに入れていましたが、 今回は tmp/ ディレクトリごと無視します。

--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+tmp/

run.sh という名前で保存して chmod で実行権限を付けて、 こいつにアセンブリのファイルを渡すと、 VM での実行までやってくれます。

$ chmod u+x run.sh

$ ./run.sh 07_if.vga.txt 
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
(略)