RubyをWhitespaceにトランスパイルする
前回までのお話
Whitespaceというプログラミング言語があります。 Whitespaceは空白文字だけで構成されたプログラミング言語です。
空白文字だけで構成されているので、当然Whitespaceでプログラムを記述するのは大変です。 そこで、RubyをWhitespaceにトランスパイルできるようにしてみました。
インストール
このトランスパイラはakazaというgemとして提供されています。 なおakazaを使うにはRubyのtrunk(パターンマッチが使えるRuby)が必要です。
$ gem install akaza
トランスパイルをする例
まずは例を見てみましょう。次はFizzbuzzを出力するWhitespaceのプログラムにトランスパイルできるRubyのプログラムです。
def fizz put_as_char 'f' put_as_char 'i' put_as_char 'z' put_as_char 'z' end def buzz put_as_char 'b' put_as_char 'u' put_as_char 'z' put_as_char 'z' end max = get_as_number n = 0 - max while n < 0 x = max + n + 1 if x % 15 == 0 fizz buzz elsif x % 3 == 0 fizz elsif x % 5 == 0 buzz else put_as_number x end put_as_char ' ' n = n + 1 end
これをトランスパイルしてみましょう。上のコードをfizzbuzz.rb
として保存し、次のコードを実行します。
$ ruby -rakaza -e "print Akaza::Ruby2ws.ruby_to_ws(File.read('./fizzbuzz.rb'))" > fizzbuzz.ws
これでfizzbuzz.ws
というWhitespaceのプログラムが生成できました。
このプログラムをakazaで実行してみましょう。
$ echo 15 | ruby -rakaza -e "Akaza.eval File.read('fizzbuzz.ws')" 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz
Fizzbuzzが表示されました。
言語仕様
akazaがトランスパイルできるのは、構文的にはRubyのサブセットで、一部の構文に限られています。 例えばレシーバーがついたメソッド呼び出しはakazaではトランスパイルできません。
次にトランスパイルできる言語の仕様を簡単に示します。詳しくはコードを読んでください。
ビルトインメソッド
Akazaは次の4つのメソッドを提供しています。
get_as_char
get_as_number
put_as_char
put_as_number
それぞれ標準入力から1文字 or 数値を取得するメソッドと、標準出力に1文字 or 数値を出力するメソッドです。
なお、これらのメソッドはRubyには存在しません。そのためakazaにトランスパイルさせるためのRubyのコードは、単純にRubyのインタプリタに渡しても実行できません。 ただしこれらのメソッドを別途定義すると、RubyのインタプリタでもakazaにトランスパイルさせるためのRubyのコードを実行可能になります。
# たとえばこのメソッド群をfizzbuzz.rbに足すと、rubyインタプリタで実行できる。 def get_as_char $stdin.read(1) end def get_as_number gets.to_i end def put_as_char(ch) print(ch) end def put_as_number(num) print(num) end
ユーザー定義メソッド
ユーザーは自由にメソッドを定義できます。 ただし制限として、引数には通常のデフォルト値のない引数のみを使用できます。
def put_3_times(ch) put_as_char ch put_as_char ch put_as_char ch end
メソッド呼び出し
Rubyと同様に呼び出せます。 ただし引数の個数チェックは行われません。引数の個数を間違えると、多分破滅します。
def foo put_as_char 'a' end def bar(a, b) put_as_number a put_as_number b end foo bar(1, 2)
リテラル
整数、および文字リテラルが使用できます。
文字リテラルはコードポイントが数値として扱われます。つまり'A'
は65
と等しいです。
put_as_char 'A' # => 'A' put_as_number 'A' # => 65 put_as_number 42 # => 42
複数文字の文字列リテラルは使えません。
ローカル変数への代入
ローカル変数に値を代入できます。 変数への代入は、Whitespaceではヒープへの値のセーブとして扱われます。
x = 1 y = 'a' z = x
Rubyと同様メソッド呼び出しによって変数のスコープが作られます。
ただし実装の都合上変数を未定義にできないため、スコープ外から変数にアクセスすると0
が入っています。
x = 1 def foo x = 2 end foo put_as_number x # => 1
if / unless
if
やunless
が使えます。後置if
もokです。
x = 0 if x == 0 put_as_number x else put_as_number 2 end put_as_number 3 unless x < 0
なお、条件文に使えるのはなにか == 0
もしくはなにか < 0
の2つの形式のみです。<=
などは使えません。
while
while
が使えます。while
もif
と同様、条件文に使えるのはなにか == 0
もしくはなにか < 0
のみです。
x = -10 while x < 0 put_as_number x x = x + 1 end
四則演算 + 剰余
四則演算と剰余ができます。
put_as_number 1 + 2 put_as_number 1 - 2 put_as_number 1 * 2 put_as_number 4 / 2 put_as_number 10 % 3
実装
RubyのコードをRubyVM::AbstractSyntaxTree
でパースして、その結果をパターンマッチを使いつつWhitespaceのコードに落としています。
詳しくはコードを読んでください。
akaza/ruby2ws.rb at 6d31edf29fd32c1287e2e3121e2871a7e04d1f87 · pocke/akaza · GitHub
まとめ
Whitespaceへのトランスパイラを書くのは結構楽しかったので、やってみてはいかがでしょうか。
このトランスパイラはWhitespaceのインタプリタを書いている時に「テスト用にWhitespaceのコードを書くのしんどいな…」という気持ちから生まれました。 Whitespaceのインタプリタを書く人にはもしかしたら役に立つかも知れません。