As Sloth As Possible

可能な限りナマケモノでありたい

タグ:esoteric

こんにちは、「それは一体誰得なんだ」でお馴染みのfaultierお兄さんだよ!今日はみんな大好きMacRubyをどれだけ無駄遣いできるかを考えてて例のごとく失敗したので、その顛末を教えてあげるよ!

MacRubyでDTを動かしたい

まぁ冒頭書いた通りなんだけど、「Objective-CからMacRubyを利用する - Watsonのメモ」を読んでなんか変なことできないかなーと考えてて、そういや俺ってば見た目に面白い以外は全く使い道のないものを以前作ってたじゃん、と思い出したんだけど、上手くいかなったという話。あ、全く使い道の無いものってのは、もちろん言うまでもなくあいつのことですね。

esotericは構成としては、ソースコードをパースしてSexpにするParserと、それをRuby2Rubyを使ってRubyのコードにトランスレートしてから実行するRunnerでできているので、MacRuby Frameworkを使ってesotericをObjCから呼び出せば、アプリケーションにDTやてってってーでプラグインを書ける仕組みを比較的容易に導入できるかと思います。導入したところで誰が使うのかわかりませんが。少なくとも俺は絶対に使わない。

まずは小手調べのコンパイルエラー

とりあえずMacRubyをDownloadしてくる。最新版の0.5はSnow Leopardにしかインストールできないけどこないだクリーンインストールしたばっかりだから全然問題ないもんね!と勝ち誇ってみせたけど、一体誰に勝ったのかはよくわからない。ちなみにソースからのビルドも時々試みてるけど大体こけるので今回は無難にバイナリをインストール。macgemは0.4のときはまともに使えたもんじゃなかったのでちょっと不安だったけど、Ruby2Rubyも特に問題なく入った様子。なに、こんな拍子抜けするくらいさらっと入っちゃっていいの?とニヤニヤしながら次のコマンドを実行。

$ pwd
/Users/taro/Projects/esoteric
$ echo $RUBYLIB
lib:
$ macruby -v 
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macruby bin/dt -v
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:13: end pattern with unmatched parenthesis: /((?:\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1\xE3/
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:74: end pattern with unmatched parenthesis: /(\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1/
dt.rb:3:in `<main>': compile error (SyntaxError)
    from dt:4:in `<main>'

オゥフ。言われたところを見てみたら、parser.rbの13行目には/((?:ど|童貞ちゃうわっ!)+)…/という正規表現が書いてあった。念のため試してみたけど、Ruby 1.9.1ではちゃんと動いてる。どうも、()の中にASCII以外の文字が含まれてるとMacRubyさんは閉括弧を見つけられなくて正規表現として不正だと言ってくる様子。ソースコードはutf-8で書いてあって、magic commentにもutf-8って指定してて、文字列リテラルだと問題ないのに、正規表現だと駄目。仕方ないのでベタに日本語書いてたところを全部Unicodeリテラルにしてみた。"ど"だったら"\u3069"とか。とりあえずそれでコンパイルできないというエラーは出なくなった。CRubyの方でももちろんちゃんと動く。なんだよ、やればできるんじゃないか、ツンデレか?などと思いつつhi.dtを実行させてみる。

$ macruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ macruby bin/dt examples/hi.dt
parser.rb:160:in `numeric:': ArgumentError (ArgumentError)
from parser.rb:80:in `process'
from parser.rb:58:in `block'
from parser.rb:51:in `parse'
from parser.rb:11:in `parse:'
from runner.rb:25:in `run:'

ぬぅ。まだツンツンしてやがる。ちょっと勢い込んでしまったけど、どうもまだMacRubyと打ち解けきれてないみたい。ちなみに、CRubyの方でやるとこんな感じになる。

$ ruby -v
ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-darwin10.0.0]
$ ruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ ruby bin/dt examples/hi.dt
$stack = []
$heap = {  }
$stack.push(72)
$stdout.print($stack.pop.chr)
$stack.push(105)
$stdout.print($stack.pop.chr)
$stack.push(33)
$stdout.print($stack.pop.chr)
$stack.push(10)
$stdout.print($stack.pop.chr)
exit(0)
Hi!

うーん、ちゃんと動いてるよなぁ。該当の箇所を調べたら、本来encodingがUTF-8のStringが来てなきゃいけないところで、MacRubyの場合はUS-ASCIIなStringが来てしまっている。あるぇ?その文字列がどっから来てるかを辿って行くとARGF.readしてるとこなんだけど、MacRubyでは既にその時点でUS-ASCIIとして読み込んでしまっている。CRubyでやったらちゃんと動くのだから、$stdin.external_encodingはちゃんとUTF-8になるはずなんだけど、そもそもそこがnilだし、opneとかset_encodingとかで指定しても変化なし。force_encodingとかしても上手くいかない。と、このあたりでもっと色々なことがおかしいということに気付く。

MacRubyでStringが期待した挙動をしてない件

いまいち良くわからないので、試しにこんなことをしてみた。

$ cat test_string.rb
# coding: utf-8
a = "ど"
b = "\u3069"
puts "\"ど\".encoding        #=> #{a.encoding}"
puts "\"\\u3069\".encoding    #=> #{b.encoding}"
puts "\"ど\" == \"\\u3069\"     #=> #{a == b}"
puts "\"\\u3069\" == \"\\u0069\" #=> #{b == "\u0069"}"
puts "\"ど\" =~ /\\u3069/     #=> #{a =~ /\u3069/}"
puts "\"i\" =~ /\\u3069/      #=> #{"i" =~ /\u3069/}"
puts "\"i\" =~ /\\u0069/      #=> #{"i" =~ /\u0069/}"
$ ruby testb_string.rb
"ど".encoding        #=> UTF-8
"\u3069".encoding    #=> UTF-8
"ど" == "\u3069"     #=> true
"\u3069" == "\u0069" #=> false
"ど" =~ /\u3069/     #=> 0
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0
$ macruby test_string.rb
"ど".encoding        #=> UTF-16
"\u3069".encoding    #=> US-ASCII
"ど" == "\u3069"     #=> false
"\u3069" == "\u0069" #=> true
"ど" =~ /\u3069/     #=> 
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0

おぉう…どういうことなの…なんでこんなに違うの…。ちゃんとわかってないんだけど、こんな感じなのかしら。

  • MacRubyはソースコードがUTF-8で書かれているものと想定して、それをUTF-16に変換している?あと、magic commentを見てないようで、試しにeuc-jpで書いてみたらバイト列をそのままUTF-16の文字列だと解釈してStringクラスにしていて、化ける。
  • IOからの読み込みはASCIIとして扱っている。ARGFでもopenでも同じだった。こちらも環境変数、コードのencoding、magic comment、読まれるファイルのencodingに関わらず同じ。
  • String#encodeやString#force_encodingが何もしないでselfを返してるように見える。NSStringのメソッドを使って変換してやれば変わるんだろうか?
  • Unicodeリテラルを解釈するときに、\uXXXXの後ろ二桁しか見てないっぽい。"\u3069" == "\u0069"がtrueって何の冗談かと思った。
  • Unicodeリテラルの扱いが、文字列リテラルの中なのか正規表現リテラルの中なのかで違っている。"i" == "\u3069"はtrueだけど"i" =~ /\u3069/はfalse。そう言えば、"(ど)"は正しくパースできるのに/(ど)/はSyntaxErrorになるところを見ると、Unicodeリテラルに限らずそもそもそこのパースのロジックが微妙に統一されてない感じ。

さてどうしたもんかな…。日本語を正規表現でマッチさせてるところがまずい(ちなみにBrainf*ckは完全に、Whitespaceは不完全ではあるけど一応動いたので、ソースコードと入力にNON-ASCIIな文字列が含まれてなければ問題ないらしい)なら、完全にバイト列だと思って扱ってやるとか、正規表現じゃなくて==しか使わないとか(もちろんバイト単位で比較)、ObjCでまず入力を正規化してやった上でMacRubyに渡すとか(本末転倒!)、そういう風にすれば動かないでもないかもしんないけど、そういう文字列処理みたいなObjCであんまり書きたくないところをRubyでさらっと書けるから良いんであって、それ以外のところはそもそもObjCで書いたって大して難しくない。performSelectorとかランタイムAPIとか使いまくればいいんだよ!というわけでちょっと残念な感じ。

余談

esotericに付属のesocコマンドを使うとDTやBrainf*ckのコードをRubyのコードに変換できるので、出来たコードをmacrubycにかけてやれば最終的にMacOSXで動作するバイナリができます。DTのコードがなんと高速で動作するネイティブのバイナリに!…と思ったけど結構遅かった。なんかこう、色々読み込むののオーバーヘッドが馬鹿にならない感じ。でも、普通にRubyを使うとstack level too deepで動かないような深い再帰のコードでも動いたりする。ていうかexamples/fact.*、macrubycでコンパイルしないと動かないんですけど。何でこんなコード入れてんだ俺。

ここ数日、LLVMについて少し勉強している。そもそもなんでLLVMを触り始めたかというと、Twitter上で「今コンパイル欲求に駆られている」と(割と何も考えずに)つぶやいたところ、

  1. 「じゃあDTコンパイルしようぜ」
  2. srd!
  3. でもあれコンパイラとは名ばかりでぶっちゃけ文字列をRubyコードにtranslateしてるだけだったりするね
  4. 「DTパーサを改良してLLVMにブリッジして、クロスプラットフォーム環境で高速に動作するDT処理系にするといいよ」
  5. 何その無駄に敷居高いお仕事!誰得!でもなんか面白そう!

というやりとりがあって、じゃあ本当に誰が得するのかわからないけど面白そうだからLLVMをバックエンドで使ってesotericがサポートしてるコードからバイナリを生成するコンパイラ作ろうぜ、という流れになったから。実にLLVMの無駄遣いですね。この記事ブクマするときは「LLVMの無駄遣い」ってタグを付けるといいですよ。

LLVMって何さ?

まずそもそも、iPod touchのJail Breakに手を出そうとしたときにちょこっと調べたりした(iPhoneSDK発表以前、JBしたiPod touch/iPhone向けにアプリを開発する際にはLLVMを使ってiPhone OS向けにコンパイルする必要があった。最近は追ってないので今どうなってるかは知らない)程度だったので、実のところLLVMが何をするものなのかちゃんと分かってなくて、どうすんべーとまず調べ直すところから始める。

LLVMとはLow Level Virtual Machineの略で、Lightweight Languageとは特に関係がないようだ(お約束)。知ってたよ。うん。知ってたってば。調べた結果、コンパイラ基盤と呼ばれるものでコンパイラを作るのに使うライブラリ・ツール群であること、VMの名の通り一度LLVMで処理できる中間言語にした後、それをLLVMのツールで実行したりネイティブのバイナリに変換したりして使うということ、コンパイル時や実行時に様々な最適化を施してくれて、より実行効率の良いプログラムを作れるようにしてくれるらしいこと、なんかが分かった。他にも色々あるけど、それはきっと詳しい人がどっかでまとめてくれてるから割愛。うん。分かんなかったわけじゃないよ。違うってば。

あと、llvm-gccとかclangとかを使えばC/C++/Objective-CのコードをLLVMに対応させてコンパイルできるようだ。それはそれで面白いけど今回は「自作の言語をコンパイルするのにLLVMを使う」のが目的なので、どっちかというとそれ相当のものを作ることになるのでとりあえずその辺は後で見ることにする。DTコンパイラをllvm-gccと並べてよいものかとかそこにツッコミを入れるのは無しの方向で。石を投げたりしないで下さい。

Rubyから使う

大雑把に何なのかはわかったけど、さてどう使おう。C++では使えるようだしサンプルもC++のコードが並んでるんだけど、C++書いたことないしなぁ、ってかRubyで書いてたのをC++を勉強しつつ書き直すとか面倒くさいなぁ、むしろObjC書きたいなぁObjC、とか段々脱線しはじめたときに、RubyからLLVMを使うgemがあるのを見つけた。

これこれ、これが欲しかったのよと早速gem installしてみたところ、コンパイルは通ってちゃんとインストールできるんだけど、サンプルコードを動かしてみたところdylibのload errorが出る。MacOSX10.5のRuby1.8.7(LLVM、RubyともにMacPortsからインストール)でしか試してないけど、どうも上手く行かない様子。調べてみようと思ってgithubからソース落としてきて、ローカルでgemをビルドするところからやってみたら、そっちはちゃんと動くようになった。あれかなぁ、リンクするライブラリとか違うとこ見ちゃうのかな。まあいいや、とりあえず深追いするのはもう少し後にしよう。

大雑把にllvmrubyの使い方を説明すると、大体以下の通り。

  1. LLVM::Moduleのオブジェクトを作る
  2. LLVM::Module#external_functionで外部のライブラリの関数を使えるようにする
    • CやC++のライブラリから取ってこれるようなので、Rubyの組み込み関数とかも呼べる
  3. LLVM::Module#get_or_insert_functionで関数を定義する
    • 上のメソッドがLLVM::Functionのオブジェクトを返すので、それのcreate_blockメソッドでブロック(関数の中身)を用意する
    • ブロックのBuilderを取得して、それに対してごにょごにょして中身を作る
    • b = method.create_block.builderですね
  4. LLVM::Module#write_bitcodeでコンパイルしてファイルに書き出すか、LLVM::ExecutionEngineのrun系メソッドでそのまま実行する

ちなみにこれはLLVMのバイトコードを生成する手順だけど、他にRuby1.9のVMの命令セット互換の中間表現を実行するruby_vm.rbが添付されてたり、Rubyのコードをllvmrubyを使ってJITコンパイルするものだと思われる(ちゃんと見てないので間違ってたらツッコミ歓迎)yarv2llvmってgemがあったりする。ただまぁ、「DTをRuby上で高速に実行したい」のではなくて「DTからスタンドアローンのネイティブプログラムを生成したい」ので、こっちは今回は使わない。Cでの拡張をしないアプローチでRubyのプログラムを高速化したくなったらこの辺のを使うのかな、多分。ruby_vm.rbの方は後でVMを実装するときに参考になりそうだからそのときにまた読もう。

ぐぬぬ

ここまでは何とか辿りついた。実はここまでで結構試行錯誤してるけど、まぁ何とかここまではよしとする。で、だ。ここで問題なのは、前回のバージョンアップで中間表現をRuby2Rubyで扱えるASTにしてしまったこと。何が問題かって、これのコンパイラを作るってのはつまりRubyのコンパイラを作るってことになるわけですよね。うん。…できるかっ!

そもそも言語処理系の基礎知識も何もない状態でネタで始めた言語作り、いきなりRubyのASTを処理できるコンパイラを作るとか正直敷居が高すぎるので、もう一度中間表現の見直しをすることにした。もっとシンプルなASTにしておけばコンパイラの方でも扱いやすいし、前回ぶっ殺したEsotericVMの方ももう少し楽に作れるかもしれない。

そこで今少しずつ仕様の見直しをしつつコンパイラを書きつつ進めてるのだけど、Low Level Virtual Machineと言うだけあってライブラリ側では大分コアなところしか用意はしてくれないわけですよね。てことで諸々のデータ型とか最低限必要な組み込みの関数とか自分で用意してやらなきゃいけないわけで、その辺考えてたら大分混乱してきた。

どう考えてもDTにオブジェクトシステムとかGCとかは要らないので適当に端折ろうとは思うのだけど、esotericはパーサ部分で大方の言語固有の要素を解決しちゃってコンパイラやVMの方は汎用的な言語処理系の基盤にしたいわけで、そうすると一応はちゃんと考えて設計しないといけないよなぁ。いかにミユビナマケモノとはいえそろそろノリでなんとかできる範囲を超えてきた感じ。助けてくだしあ。とりあえずお薦めの書籍とか誰か教えてくれると嬉しいなぁ。情報系の学生が読むようなの。

こないだ作ったesotericがあろうことか0.0.2にバージョンアップしました。主な変更点は以下の通り。

  • 各言語をコンパイルしてできる中間コードが、前のバージョンでは似非アセンブラ的な何かだったものを、ParseTreeなんかで作るようなRubyの抽象構文木(AST)的な何かに変わった。VMでの実行前に生成されたコードを最適化するとかできるようになるとか、他のRubyライブラリにEsoteric Languageパーサが埋め込めるようになるとか、無駄に夢が広がる感じで。
  • もちろん似非VMでは動かなくなったので、作り直さなきゃいけないんだけど、ちょっと時間かかりそうなので似非VM殺した。そのかわり、ASTはRuby2RubyとかでRubyコードに変換できるように作ってあるので、とりあえずRuby2Rubyを使うようにして誤魔化すことにした。
  • 副産物としてDTやWhitespaceのコードを実行可能なRubyスクリプトに変換するコンパイラもどきのツールが出来た。Cへのトランスレータもあるみたいだし、そういうの使えば本当にコンパイルしてバイナリも作れたりしますね。DTでコマンドとか作れるようになるね。まったくやりたくないけどね。
  • Brainf*ckのパーサを追加
  • てってってーのパーサを追加。一応一通りの仕様は満たしてると思うけど、「「\xAB」「\uABCD」「\d00000」:エスケープシーケンスとして、それ全体で1文字と扱われる。(予定)」ってのはまだ実装してない。あと「ててー」と「てっー」が微妙な動きをするのを解決してないけど、まぁそのうちなんとかする。

とまぁ、そんな感じ。次はまず、VMの再実装と、あとSpec書く。それ終わったらRubyで作る奇妙なプログラミング言語 ~Esoteric Language~に載ってたStarryやBolicでも実装しようかな。あとはとかやってみようかしら。

大分楽しくなってきたところだけどアイマスSP買ってきてしまったので、一回休み。そしたら来月ぐらいから仕事が忙しくなるに違いないのでまた一回休み。一段落ついたら多分飽きてるだろうからまた(ry。まぁまったりもったりやりますかねぇ。

↑このページのトップヘ