Ruby
Gem
RubyDay 9

コードの意味を解説するインタプリタを作ろうとした話

去る10月に彼女がいないRubyistに送るインタプリタとして公開したRubieを作ったときのことを書きます。

本題

作成に至ったきっかけ

なんか変わったgemを作ろうと思った結果、ペルソナとして彼女のいないRubyist(というよりRubyが彼女というRubist)が思いつき、ならRubyが喋ってくれたらいいよねっとなったからでした。

開発の経過

STEP1 プロジェクトの作成

gemを作成するプロジェクトを作成しました。

$ bundle gem rubie -t

tオプションによってテスト(RSpec)付きのプロジェクトが作成できます。
この後、rubie.gemspecで諸々設定するのですが、本題ではないので省略します。ただ下記のコードはあると公開できない仕組みになっているので消しておきます。

# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'      
# to allow pushing to a single host or delete this section to allow pushing to any host.        
if spec.respond_to?(:metadata)      
  spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"      
else        
  raise "RubyGems 2.0 or newer is required to protect against " \       
  "public gem pushes."      
end

STEP2 とりあえずインタプリタとして動くようにする

Rubyにはevalメソッドが備わっているので、実は適当なインタプリタでよければとても簡単に作成できます。一応知らない人もいるかもしれないので紹介して置くと、evalメソッドは引数に文字列を取り、その文字列をRubyのコードとして実行するメソッドです(返り値はevalによって実行されたコードの返り値です)。
少し例をみて見ましょう。

eval("puts 'hello world'") # hello worldと表示される
a = 10
eval('puts a') # 10と表示される
eval('a -= 5')
puts a # => 5

はい、本題に戻ります。適当なインタプリタでよければ次のコードで動きます。

while true
  command = gets
  puts eval(command)
end

getsメソッドは標準入力から入力を受けるメソッドです。
これを実行するとほぼインタプリタとして動きますが、少し足りません。試しに適当に動かすとわかるかもしれません。例えば、a = 10と入力した後にaを実行するとundefined local valiable or methodとエラーが出るはずです。同様にメソッドを宣言して呼び出してもエラーになるはずです。
どうやらローカル変数の記憶ができてないみたいです。(実はインスタンス変数は動きます)

STEP3 変数の記憶できるようにする

ではどうすれば、ローカル変数を記憶できるのでしょうか。これにはRubyの仕組みを利用します。細かくは理解できていないのですが、Rubyではローカル変数とインスタンス変数は記憶の仕方が違います。インスタンス変数はインスタンスに記憶されます。ローカル変数はBindingクラスのインスタンスに記憶されます。スコープが変わる度にBindingのインスタンスが作られます。
そこでBindingのインスタンスを作りましょう。bindingメソッドで作ってしまうとインタプリタ側の変数と干渉してしまうので却下です。Binding.newで作れるのが一番楽ですが、Bindingにはnewメソッドが存在しないのでインスタンスを作成できません。
Objectを作ってからbindingメソッドで呼び出す必要があります。
さてevalには第2引数として、Bindingクラスのオブジェクトをとっています。こうすることで、そのBindingが作れられたスコープ下にて実行されたことと同じになります。

以上をまとめます。

world = Object.new.send(:binding)
while true
  command = gets
  puts eval(command, world)
end

STEP4 コードの意味を解説してもらう

自力でパーサを作るとしたら辛かったのですが、Rubyには標準ライブラリにパーサがあります。Ripper.parseでパースできるので、あとは再帰で頑張って文章にします。
https://github.com/sasurai-usagi3/rubie/blob/master/lib/rubie/ripper.rb

STEP5 コマンドとして実行するために

binディレクトリの中にファイルを作成しましょう。

STEP6 公開へ

リリースできる状態(ビルドが通る)か確認しておきましょう。rake buildを実行して問題ないかみておきます。
最後にrake releaseでリリースできます。

終わりに

ここまでを頑張って四則演算だけの式を解説できるインタプリタが作成できました。まだまだやることがいっぱいなのでこれからも開発を進めていきます。
最後に、Rubieは公開済みでオープンソースのgemなのでPull Reqお待ちしてます!
- https://rubygems.org/gems/rubie
- https://github.com/sasurai-usagi3/rubie