Rubyでできる!MinRubyコンパイラの作りかた
こんにちは。永和システムマネジメントの内角低め担当、畠山です。
RubyでMinRubyのコンパイラを作って遊んでみたので、それをご紹介します。
MinRubyとは?
「RubyでつくるRuby」という本をご存知ですか?「RubyでRubyインタプリタを作りながらRubyを学べる」という意欲的な本です。この本の中で作成する、機能を限定したRubyのサブセット言語が「MinRuby」です。
RubyでつくるRuby
Rubyで学ぶRuby(書籍のベースとなったアスキーjp上の連載記事)
MinRubyのパーサ(構文解析器、プログラムのソースコードを構文木に変換する)はRubyのライブラリとして提供されています。今回は手軽にコンパイラを作成したいので、MinRubyのパーサをそのまま利用します。
MinRubyのパーサの使い方はこんな感じです。
https://gyazo.com/88d457df5df40d65018af582ca5d7102
本家MinRubyとの違い
コンパイラの実装を単純にするためにいくつか手抜きをしています。そのため、本家のMinRubyと以下の差異があります。
integer 以外の型はサポートしない
関数の引数は6つまで(6つを超えると引数をスタックへ積む必要がある)
ArrayとHashは未実装(あとで書きたい)
開発環境
今回はDocker上に構築したLinux / x86_64の環境で開発を行います。
整数リテラル
まずは整数リテラルを評価し、画面に表示するだけのコンパイラを作成してみます。
一番最初のMinRubyのプログラムはこんな感じです。
code:4649.rb
4649
コンパイラのコードはこんな感じ。["lit", 整数リテラル] が来たらRAXレジスタに整数リテラルの値をそのままセットし、終了直前にRAXレジスタにセットした値を画面に表示します。
code:minrubyc.rb
require "minruby"
tree = minruby_parse(ARGF.read)
puts "\t.intel_syntax noprefix"
puts "\t.text"
puts "\t.globl main"
puts "main:"
puts "\tpush rbp"
puts "\tmov rbp, rsp"
else
end
# 入力した整数をプリントする
puts "\tmov rdi, rax"
puts "\tcall p"
puts "\tmov rsp, rbp"
puts "\tpop rbp"
puts "\tret"
整数を画面に出力するための p 関数はライブラリ関数として libminruby.c で定義しておきます。
code:libminruby.c
long p(long n) {
printf("%ld\n", n);
return n;
}
それでは 4649.rb をコンパイルしてみます。以下のようなx86-64アセンブリが出力されるはずです。
https://gyazo.com/0fc1536fafc7181fe562647e085cc350
このアセンブリファイルを tmp.s という名前で保存し、
code:sh
$ ruby minrubyc.rb 4649.rb > tmp.s
Docker環境の中へ入ってビルド & 実行します
code:sh
$ docker run --platform linux/amd64 -it -v pwd:/app -w /app ruby:latest bash
# gcc -z noexecstack tmp.s libminruby.c -o 4649
# ./4649
以下のように「4649」が画面に表示されればOKです。
https://gyazo.com/7cbadc2330b3feb7e7750890149f2086
四則演算
次は四則演算を計算できるようにコンパイラを拡張します。四則演算(+ - * /)が来た場合、「左辺を評価→右辺を評価→左辺と右辺との演算結果をRAXレジスタへ格納」を行います。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index f2bcade..ab8d98a 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -1,5 +1,38 @@
require "minruby"
+def gen(tree)
+ elsif %w(+ - * /).include?(tree0) + # 左辺を評価して結果をスタックへ積む
+ puts "\tpush rax"
+ # 右辺を評価して結果をスタックへ積む
+ puts "\tpush rax"
+ # スタックに積んだ右辺の値をrdiへセット
+ puts "\tpop rdi"
+ # スタックに積んだ左辺の値をraxへセット
+ puts "\tpop rax"
+ # 演算する
+ when "+"
+ puts "\tadd rax, rdi"
+ when "-"
+ puts "\tsub rax, rdi"
+ when "*"
+ puts "\timul rax, rdi"
+ when "/"
+ puts "\tcqo"
+ puts "\tidiv rdi"
+ else
+ raise "invalid operator: #{tree0}" + end
+ else
+ end
+end
+
tree = minruby_parse(ARGF.read)
puts "\t.intel_syntax noprefix"
@@ -9,11 +42,7 @@ puts "main:"
puts "\tpush rbp"
puts "\tmov rbp, rsp"
-else
-end
+gen(tree)
# 入力した整数をプリントする
puts "\tmov rdi, rax"
diff --git a/test.sh b/test.sh
index 9fc63db..33f4672 100755
--- a/test.sh
+++ b/test.sh
@@ -19,5 +19,10 @@ assert() {
assert 4649 '4649'
assert 5963 '5963'
+assert 30 '10 + 20'
+assert 10 '30 - 20'
+assert 63 '7 * 9'
+assert 6 '30 / 5'
+
echo OK
複文の実行
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index ab8d98a..a210442 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -28,6 +28,20 @@ def gen(tree)
else
end
+ stmts.each do |stmt|
+ gen(stmt)
+ end
+ elsif tree0 == "func_call" && tree1 == "p" +
+ # 第一引数を評価
+
+ # 関数を呼び出し
+ puts "\tmov rdi, rax"
else
end
@@ -44,10 +58,6 @@ puts "\tmov rbp, rsp"
gen(tree)
-# 入力した整数をプリントする
-puts "\tmov rdi, rax"
-puts "\tcall p"
-
puts "\tmov rsp, rbp"
puts "\tpop rbp"
puts "\tret"
diff --git a/test.sh b/test.sh
index 33f4672..3f58195 100755
--- a/test.sh
+++ b/test.sh
@@ -1,7 +1,8 @@
assert() {
- expected="$1"
+ # expected の "\n" を改行コードをとして解釈させる
+ expected=echo -e "$1"
input="$2"
echo "$input" > tmp.rb
@@ -9,7 +10,7 @@ assert() {
docker run --platform linux/amd64 --rm -v ${PWD}:/root -w /root gcc:latest gcc -z noexecstack tmp.s libminruby.c -o tmp
actual=docker run --platform linux/amd64 --rm -v ${PWD}:/root -w /root gcc:latest ./tmp
echo "$input => $actual"
else
echo "$input => $expected expected, but got $actual"
@@ -17,12 +18,12 @@ assert() {
fi
}
-assert 4649 '4649'
-assert 5963 '5963'
-assert 30 '10 + 20'
-assert 10 '30 - 20'
-assert 63 '7 * 9'
-assert 6 '30 / 5'
-
+assert 4649 'p 4649'
+assert 5963 'p 5963'
+assert 30 'p 10 + 20'
+assert 10 'p 30 - 20'
+assert 63 'p 7 * 9'
+assert 6 'p 30 / 5'
+assert "10\n20\n" 'p 10; p 20'
echo OK
変数を導入