###はじめに
この記事はRuby Advent Calendar 2020 22日目の記事です。遅刻して申し訳ありません。
Rustという言語でrurubyというRuby処理系の独自実装を作っており、今回はその紹介をします。RustでRubyということでrurubyという安直なネーミングにしましたが、発音しずらい上に本家Rubyと紛らわしく恐れ多いので後悔しております。もう少し技術的なポエムを言語実装Advent Calendar 2020に書きましたので、興味のある方はそちらもご参照頂ければ。
###Rustについて
Rustは比較的新しいプログラミング言語で、C言語と同様の静的型付け言語です。最新の研究成果を取り入れた柔軟で堅固な型システムと型推論機構を持ち、高速かつメモリ安全・スレッド安全な言語となっています。また、高度な抽象化機能を実行速度を犠牲にしないやり方でうまく取り入れていて、言語処理系のようなシステムプログラミングには最適です。
逆に、所有権システムという独特の仕組みがあってプログラマに一定の制約が課されます。コンパイルが通れば安全ですが、慣れるまではコンパイルを通すまでが大変、と巷では言われています。プログラムの書きやすさは犠牲にして高速性と安全性を追求するという、ある意味Rubyとは正反対の思想のプログラミング言語ですね。(どちらの言語が良いか、という不毛な議論はするつもりはありません。場面に応じて自分が使いやすいものを使えばよいと思います。念のため。)
###実装の現状について
基本的には現在のRubyと同様にプログラムを中間表現(バイトコード)にコンパイルして仮想マシンが解釈実行するタイプの処理系です。パーサは独自に古典的な再帰下降パーサを心を込めて手書きで書いています(つらい)。不要になったオブジェクトを回収するガーベジコレクタも、単純なものですが自前で実装しています。
現在の進捗状況ですが、ほとんどの制御構文やリテラルは動きます。組み込みクラスは基本的なものは実装していますが、まだまだクラスもメソッドも足りていないのと、特殊変数など、まだ適当な実装になっている部分が多数あります。
Rubyベンチマーク界のゴールデンスタンダードであるOptcarrotも動きます。実行速度の方も頑張ってはいますが、残念ながらRuby3.0.0-previewの2.7倍、--optという自己書き換えで高速化するオプションをつけた場合は2.1倍遅いです。いろいろなベンチマークの結果はこちら
今年の夏に話題になった大塚製薬のカロリーメイト・リキッドのプログラマ向けCUIサイトに隠されている人間にはとても読めないひどい超絶技巧を駆使したプログラムであるQuineも動かしました。
動きカクカク問題、@Linda_pp さんの助言により完璧に動作するようになりました!(嬉しかったのでしつこく投稿) pic.twitter.com/tvPV1K4a3i
— monochrome (@s_isshiki1969) August 18, 2020
Rubyの標準ライブラリもrequireで読めるようになっています。ただCバインディングを使って書かれたものは動きませんし、一部文法がサポートされていないのと、メソッドも足りないので全然ダメです。実はこの原稿を書くにあたっていくつか試しましたが、ほとんど動かなかったのでへこんでいます。一つ一つ動かせるようにしていくことが当面の課題です。
###使い方
レポジトリはこちらです。
https://github.com/sisshiki1969/ruruby
❯ git clone https://github.com/sisshiki1969/ruruby.git
❯ cd ruruby
実行環境としてはLinuxを想定しています。Rustが動く環境であればWindowsでもOKなはずですが検証していません。WSL2やdocker上での実行を推奨します。
実行にはまずRustのインストールが必要です。少し手数がかかりますが、基本的にはコマンド一発で終わりますので、Rubyのビルドに比べると飛躍的に簡単です。
https://www.rust-lang.org/ja/learn/get-started
Rustはstable
/beta
/nightly
という3つのリリースチャンネル(バージョンみたいなものだと思ってください)があり、rurubyの実行にはnightly
チャンネル(Rubyでいうとpreview的なもの)が必要です。
インストールの際にnightly
を選択するか、もしくはとりあえずデフォルトのインストールを行い、その後で下記のようにnightly
をデフォルトのチャンネルとしてください。
❯ rustup install nightly
❯ rustup default nightly
レポジトリのルートでcargo run
を実行すると自動的にコンパイルされ、できた実行ファイルが実行されます。--release
オプションを付けるとコンパイルに時間がかかりますが、最適化され、高速化された実行ファイルが生成・実行されます。
実行ファイルはtarget/debug/ruruby
(--release なしの場合)かtarget/release/ruruby
(--release ありの場合)に配置されますが、cargo runを使えば適当に実行されますのであまり気にしなくて大丈夫です。
tests/
以下にいくつかベンチマーク・プログラム(多くはRubyのリポジトリ内にあるもの)がありますので試してみてください。
まずは定番、フィボナッチ数列の素朴な計算です。
❯ cargo run --release tests/app_fibo.rb
Compiling ruruby v0.2.0 (/mnt/c/Users/sissh/Documents/GitHub/ruruby)
Finished release [optimized] target(s) in 30.04s
Running `target/release/ruruby tests/app_fibo.rb`
9227465
aobenchはシンプルなレイトレーシングです。標準出力に.ppm
形式のデータを吐き出しますので、convert
コマンド(ImageMagick)などで適当なフォーマットに変換する必要があります。
❯ cargo run --release tests/app_aobench.rb > ao.ppm
Finished release [optimized] target(s) in 1.15s
Running `target/release/ruruby tests/app_aobench.rb`![aobench.PNG]
❯ convert ao.ppm ao.jpg
ファイル名を省略するとREPL(Rubyでいうirb
)が起動します。
❯ cargo run
Compiling ruruby v0.2.0 (/mnt/c/Users/sissh/Documents/GitHub/ruruby)
Finished dev [unoptimized + debuginfo] target(s) in 1m 24s
Running `target/debug/ruruby`
irb:0> a = "Ruby"
=> "Ruby"
irb:0> puts "Hello #{a} world!"
Hello Ruby world!
=> nil
エラー処理もエラーの発生場所と対処方法が分かりやすいように頑張ってスタックトレースを表示してくれます。
❯ cargo run --release tests/so_nbody.rb
Finished release [optimized] target(s) in 1m 26s
Running `target/release/ruruby tests/so_nbody.rb`
-0.169075164
NoMethodError(no method `distances' for #<Planet:0x7f7509584a00>:Planet)
0:/mnt/c/Users/sissh/Documents/GitHub/ruruby/tests/so_nbody.rb:31
mag = dt / (distances * distance * distance)
^^^^^^^^^
1:/mnt/c/Users/sissh/Documents/GitHub/ruruby/tests/so_nbody.rb:143
b.move_from_i(BODIES, nbodies, dt, i + 1)
^^^^^^^^^^^
2:/mnt/c/Users/sissh/Documents/GitHub/ruruby/tests/so_nbody.rb:139
n.times do
^^^^^
###まとめ
「Ruby処理系自作入門」と題していますが「オレの作ったRuby自作処理系への入門」になってしまいました。ご容赦ください。
言語処理系製作、大変楽しく勉強になりますので(Rustの勉強にももちろんなりますし、Rubyの実行モデルの理解も深まりますので一挙両得です)、皆さんもぜひオレオレ言語処理系製作にチャレンジしてみてください。