vm2gol v2 (53) 間接メモリ参照のフォーマットの改良 / alinesへの蓄積をやめる



間接メモリ参照のフォーマットの改良

機械語での間接メモリ参照はこれまでアセンブリでとまったく同じ [bp-2], [bp+3] のようなフォーマットにしていて、 VM では /^\[bp-(\d+)\]$/ のような正規表現を使うことで 間接メモリ参照であることを判定したり bp からの相対位置(displacement)を取り出したりしていました。

しかし、この VM は 一応なんらかの CPU のシミュレータのつもりですから (オレオレのかなりいい加減なものとはいえ……) 、 正規表現とか高級なことはなるべくさせないようにしたい。

そこで今回、以下のようにちょっとだけ機械が読みやすそうな表現に変えました。

(before)
[bp-2]
[bp+3]

(after)
ind:bp:-2
ind:bp:3

見ての通りです。 ind は indirection の略。

この変換はアセンブラで行います。

--- a/vgasm.rb
+++ b/vgasm.rb
@@ -31,6 +31,15 @@ def create_label_addr_map(alines)
   map
 end
 
+def to_machine_code_operand(arg)
+  case arg
+  when /^\[bp\-(\d+)\]$/ then "ind:bp:-#{$1}"
+  when /^\[bp\+(\d+)\]$/ then "ind:bp:#{$1}"
+  when /^-?\d+$/         then arg.to_i
+  else                        arg
+  end
+end
+
 src = File.read(ARGV[0])
 alines = parse(src)
 
@@ -49,9 +58,7 @@ alines.each do |aline|
     label_name = rest[0]
     insn << label_addr_map[label_name]
   else
-    insn += rest.map {|arg|
-      (/^-?\d+$/ =~ arg) ? arg.to_i : arg
-    }
+    insn += rest.map {|arg| to_machine_code_operand(arg) }
   end
 
   puts JSON.generate(insn)

影が薄かったアセンブラちゃんの仕事を増やせてよかったですね(?)。 base の部分には bp しか来ないので決め打ちにしています。

VM 側の修正はこんな感じ:

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -268,6 +268,20 @@ class Vm
     DUMP
   end
 
+  def calc_indirect_addr(str)
+    _, base_str, disp_str = str.split(":")
+
+    base =
+      case base_str
+      when "bp"
+        @bp
+      else
+        raise not_yet_impl("base_str", base_str)
+      end
+
+    base + disp_str.to_i
+  end
+
   def add_ab
     @reg_a = @reg_a + @reg_b
   end

@@ -283,11 +297,8 @@ class Vm
       case val
       when Integer
         val
-      when /^\[bp-(\d+)\]$/
-        stack_addr = @bp - $1.to_i
-        @mem.stack[stack_addr]
-      when /^\[bp\+(\d+)\]$/
-        stack_addr = @bp + $1.to_i
+      when /^ind:/
+        stack_addr = calc_indirect_addr(val)
         @mem.stack[stack_addr]
       else
         raise not_yet_impl("val", val)

先頭だけ見ればよいので ^ind: と簡素な正規表現で判定できるようになり、 calc_indirect_addr() でも単に split(":") で済むようになりました。

また、 displacement の正負も統一的に表現できるようになったこともあり、今回あわせて分岐も減らしています。

case 式の都合に合わせるため正規表現は残していますが、 なくそうと思えばいつでもなくせる状態になってます (たとえば 〜.start_with?("ind:") に書き直すとかで) ので、まあこれでええでしょということにします。

(追記 2021-02-06) アセンブリでのフォーマットも変更

パースがしやすいのと、 数値の正負によって挙動を切り替える煩雑さがなくなってすっきりするため、 機械語と同様にアセンブリでのフォーマットもコロンで区切る形に変えることにしました。

素人の浅知恵かもしれませんが、これではダメだということになったらまたそのとき考えましょう。

(before)
[bp-2]
[bp+3]

(after)
[bp:-2]
[bp:3]

これにより、機械語への変換処理も次のように簡素に。

# vgasm.rb

def to_machine_code_operand(arg)
  case arg
  when /^\[(.+)\]$/ then "ind:#{$1}"
  when /^-?\d+$/    then arg.to_i
  else                   arg
  end
end

コード生成の簡素化(alines への蓄積をやめる)

 def codegen(tree)
-  alines = []
-
-  alines << "  call main"
-  alines << "  exit"
+  puts "  call main"
+  puts "  exit"
 
   head, *rest = tree
   # assert head == "stmts"
-  alines += codegen_top_stmts(rest)
+  codegen_top_stmts(rest)
-
-  alines
 end

各メソッドの先頭で配列 alines を用意していた部分と alines を返却していた部分がなくなり、 アセンブリコードを alines に蓄積するのをやめてその場で出力するようにしました。

副作用がない方がテストとかしやすいかなと思って修正前のような書き方をしていたのですが、 コード生成処理に関しては結局メソッド単位のテストは書いていませんし、 その場で出力する方式に変えても特に問題ない ( 第49回 で問題ないようにした ) ので、簡単で分かりやすい書き方に変えることにしました。

移植版の方で何度か試して悪くなさそうだったので Ruby版にフィードバックした形。

その他の修正

  • rbenv local 2.6.6
    • Ruby 3.0 がリリースされたので2つ前のバージョンということで 2.6 に上げました (これまでは 2.5 で作ってました)
  • Ruby の警告に従って修正: IO#lines => #each_line
    • IO#lines is deprecated; use #each_line instead
  • vgparser.rb: インデントの修正のみ
    • 前回の後始末
  • dump_exe.rb を削除
    • 第8回 のときに書いたものだが、結局その後ほとんど使わなかったのと、 今回の修正で不要になったので。


vm2gol v2 (52) リファクタリング: 字句解析処理を別ファイルに分離



vgparser.rb で行っていた字句解析処理を vglexer.rb に分離します。

そうしなければいけない強い理由はあまりなくて、その方が他言語への移植がスムーズだった経験から、Ruby版にもフィードバックしておくか……というくらいのノリです。 あとは、個々のモジュールが小さくなっている方が威圧感がなくていいかなとか。


ファイル分割にあわせて、字句解析の結果であるトークン列をシリアライズして一度ファイルに出力する形にします。

整数値については、これまで字句解析の際に文字列から整数に変換して Token.new に渡していましたが、最終的に文字列にシリアライズするのならわざわざ 文字列 → 整数 → 文字列 のように変換しなくても、文字列のまま受け渡してしまえばいいかなと。 *1

というわけで、まずはメインの修正の準備として文字列のまま渡すように修正。

--- a/vgparser.rb
+++ b/vgparser.rb
@@ -49,7 +49,7 @@ def tokenize(src)
 
     when /\A(-?[0-9]+)/
       str = $1
-      tokens << Token.new(:int, str.to_i)
+      tokens << Token.new(:int, str)
       pos += str.size
 
     when /\A(==|!=|[(){}=;+*,])/

これにより、Token#value の型が String に統一され、静的型の言語への移植も(ちょっとだけ)やりやすくなります。

文字列から整数への変換はパーサで行うことになります。 微妙に DRY じゃないですが、まあいいか。

--- a/vgparser.rb
+++ b/vgparser.rb
@@ -131,7 +131,7 @@ class Parser
       t.value
     elsif t.type == :int
       @pos += 1
-      t.value
+      t.value.to_i
     else
       raise ParseError
     end
@@ -289,7 +289,14 @@ class Parser
     if t_left.type == :int || t_left.type == :ident
       @pos += 1
 
-      expr_l = t_left.value
+      expr_l =
+        case t_left.type
+        when :int
+          t_left.value.to_i
+        else
+          t_left.value
+        end
+
       parse_expr_right(expr_l)
 
     else

これで準備ができました。字句解析処理を vglexer.rb に移動します……が、diff は大きいので略。

vglexer.rb だけを動かして出力(シリアライズされたトークン列)を見るとこんな感じ:

$ ruby vglexer.rb gol.vg.txt | head
kw:func
ident:to_vi
sym:(
ident:w
sym:,
ident:x
sym:,
ident:y
sym:,
ident:offset

行ごとに記号と値をコロンで区切って並べるだけ。


あとは、 Parser クラスを解消して、トップレベルにメソッドが並ぶだけのスタイルにしました。簡単で分かりやすくする方向で考えるとクラスなくてもいいかなと。

テストコードの方もあわせて修正。すでに他のテストでやっているように ruby 〜.rb の形でコマンドを実行して結果を読む方式に。

# test_vgparser.rb

  def _parse(src)
    File.open(VG_FILE, "wb") { |f| f.print src }
    _system %( ruby #{PROJECT_DIR}/vglexer.rb  #{VG_FILE} > #{TOKENS_FILE} )
    _system %( ruby #{PROJECT_DIR}/vgparser.rb #{TOKENS_FILE} > #{TREE_FILE} )
    json = File.read(TREE_FILE)
    JSON.parse(json)
  end

その他の修正

  • 変数名をより適切なものに変更
    • words => insn, insns
    • op, operator => opcode
  • typo の修正

あと master から main にブランチ名を変えました。



*1:ドメインロジックではドメインに即したオブジェクトに変換して使うべき、という観点からはその都度変換した方がよいと思いますが、ここは難しく考えなくていいんじゃないかな……。

素朴な自作言語のコンパイラをKotlinに移植した


かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 12回目は Kotlin です。

やっつけなので汚いです。ライフゲームコンパイルが通ったのでヨシ、というレベルのものです。

github.com

移植元

memo88.hatenablog.com

ベースになっているバージョン: tag:51 のあたり

メモ


今回の実験は配列変数の宣言。 trial ブランチで試してみました。

確認用のコードです。今回の実験ではとりあえずこれだけコンパイルできれば OK。

// declare_array.vg.txt

func main() {
  var xs[22];
}

AST がこんな感じ。お試しなので適当。

$ ( \
>   cat declare_array.vg.txt \
>   | ./run.sh tokenize \
>   | ./run.sh parse \
> ) 2>/dev/null
[
  "top_stmts",
  [
    "func",
    "main",
    [

    ],
    [
      [
        "var_array",
        "xs",
        22
      ]
    ]
  ]
]

アセンブリコードがこんな感じ。

$ ( \
>   cat declare_array.vg.txt \
>   | ./run.sh tokenize \
>   | ./run.sh parse \
>   | ./run.sh codegen \
> ) 2>/dev/null
  call main
  exit

label main
  push bp
  cp sp bp

  # 関数の処理本体
  sub_sp 22 # ... 配列のサイズ分の領域をスタック上に確保する

  cp bp sp
  pop bp
  ret

これだけだったら少し修正するだけで済みました。

次のようなコードはまだ正しく動くようにコンパイルできません。

func main() {
  var xs[22];
  var a = 11;
}

配列がスタック上で占有している幅を考慮しないといけないのに、それをまだやってないからです。 これをやろうとするとたぶんあちこち修正しないといけなくなるので、それはまた今度ということで。

vm2gol v2 (51) 機械語コードのフォーマットを固定長風に変更



これまで機械語コードのフォーマットはこのような YAML ファイルにしていました。 (以下便宜的に「可変長風」と呼びます)

---
- call
- 1029
- exit
- label
- vram_set
- push
- bp
...

これを変更して、1行あたり1命令となるようにして、行ごとに JSON.parse でパースできるようにしました。 (以下便宜的に「固定長風」と呼びます)

["call", 1029]
["exit"]
["label", "vram_set"]
["push", "bp"]
...

どの命令でもメモリ上で占める幅が 1 になったため Vm.num_args_for も不要になってすっきりしました。

また、YAML への依存がなくなり JSON への依存に一本化できました。 Ruby でやる分には何でもいいのですが、他の言語に移植するときに若干めんどくさかったので。これでちょっと簡単になるはず。

その他の変更

メモ

元の YAML のように 1命令が複数の行(可変)となるようにしていたのは、機械語といってもバイナリではなくテキストにした上にさらに固定長風にしてしまうとさすがに簡略化しすぎで勉強にならないのではないか、みたいなことを考えていたからでした。たしか。

しかし、今になってみると別にこだわるところではなかったように思えます。こだわる必要がないのであれば簡単な方がよい、というわけで変えてしまうことにしました。

あとは 1行1命令にするとアセンブリコードとの違いがさらに小さくなってアセンブラが仕事してない感じになってしまうけどこれでいいの? ということが気になっていたのですが、そこも気にしないことにしました。


全体で 50行弱減って今このくらいの行数。

   14 common.rb
   58 vgasm.rb
  555 vgcg.rb
  509 vgparser.rb
  513 vgvm.rb
 1649 合計


memo88の2020年ブックマークランキング

blog.sushi.money

やってみました。調べてないけど9割方セルクマ 😅

ブクマしてくださった方、ありがとうございます。

memo88の2020年ブックマークランキングベスト24(累計30ブックマーク)

# タイトル
1位 きしださんのかわいいリレーショナルデータベースをRubyで写経した - memo88
2位 リレー式論理回路シミュレータを自作して1bit CPUまで動かした - memo88
3位 Rubyで素朴な自作言語のコンパイラを作った - memo88
4位 LibreOffice Drawのodgファイルから図形の情報を抜き出して使う - memo88
5位 kairo-gokko 製作メモ - memo88
6位 Ruby/Racc: パースに失敗した位置(行、桁)を得る - memo88
7位 素朴な自作言語のコンパイラをGoに移植した - memo88
8位 kairo-gokko (35) 1bit CPU 2 - memo88
9位 kairo-gokko (34) 1bit CPU 1 - memo88
10位 素朴な自作言語のコンパイラをPHPに移植した - memo88
11位 HiveQL テスト 自動化 メモ - memo88
12位 kairo-gokko (33) Dフリップフロップ 2 - memo88
13位 DockerでDigdagサーバを動かす(クライアントの簡単な動作確認用、PostgreSQL不使用) - memo88
14位 素朴な自作言語のコンパイラをPerlに移植した - memo88
15位 素朴な自作言語のコンパイラをCに移植した - memo88
16位 失敗したタスクの情報をdigdagコマンドとシェルスクリプトで取得する - memo88
17位 Ubuntu 18.04にJupyter NotebookとIRubyをインストール(pyenv, rbenv を使用) - memo88
18位 hive.server2.enable.doAs がよく分からなかったので Apache Bigtop で調べてみた - memo88
19位 素朴な自作言語のコンパイラをJavaに移植した - memo88
20位 正規表現エンジン(ロブ・パイクのバックトラック実装)をRubyで写経した - memo88
21位 素朴な自作言語のコンパイラをTypeScript(Deno)に移植した - memo88
22位 素朴な自作言語のコンパイラをDartに移植した - memo88
23位 素朴な自作言語のコンパイラをC♭に移植した - memo88
24位 素朴な自作言語のコンパイラをPythonに移植した - memo88

generated by 年間ブックマークランキングジェネレーター