pockestrap

Programmer's memo

RubyをWhitespaceにトランスパイルする

前回までのお話

pocke.hatenablog.com


Whitespaceというプログラミング言語があります。 Whitespaceは空白文字だけで構成されたプログラミング言語です。

空白文字だけで構成されているので、当然Whitespaceでプログラムを記述するのは大変です。 そこで、RubyをWhitespaceにトランスパイルできるようにしてみました。

インストール

github.com

このトランスパイラは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

ifunlessが使えます。後置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が使えます。whileifと同様、条件文に使えるのはなにか == 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のインタプリタを書く人にはもしかしたら役に立つかも知れません。