As Sloth As Possible

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

タグ:DT

なんでなのかと聞かれても「なんか暇だったから…」としか。

いや元々はこう周期的に来る「なんか仕事と全然関係ないコード書きたい」期だっただけなんだけど、Goとかで遊んでる最中にひょんなことからRustを見付けてしまってへー面白そうとか手を出してみることになり、「慣れない言語を使うときはとりあえずWhitespaceの処理系を作ってみるとなんか慣れたような気分になる」という癖でですね。なんか出来ちゃった。

記事書いてる時点の最新のnightly(v0.12.0-pre)ではコンパイル通るのを確認済み(というか多分0.12.0でしかビルドできない。流石いつβが出るとも分からないα言語、0.10.0と0.11.0と0.12.0で互換性無いし、ドキュメント見に行ったら標準ライブラリを含むほとんどのAPIがexperimentalだった)。rust-albinoの方をcloneしたら最新のrustcと対応するcargoをrustupかなんかで入れて、

cargo build
するとビルドできる。homebrewでも入れられるけどバイナリだと0.11.0、ソースからのビルドだとかなり時間かかるので大人しくnightlyのバイナリ使うがよろし。

Albinoがコマンドラインツールで、こいつがWhitespaceを実行したり逆アセンブルしたりしてくれる。最初は普通にインタプリタ作ってたんだけど、例のごとくWhitespace読めないし書けないからアセンブリ言語でもでっちあげよう、あとその昔書いたDTとかも動かしたいよねー、とかやってたらいつの間にかVMとパーサが分離して副産物として何故かライブラリになっちゃったのがWhitebase

厳密に言うと本体はWhitebaceの方でAlbinoはただのインターフェースだったりするけど細かいことは気にしない。先に名前が決まったのはWhitebaseの方で、Whitespaceの命令セットを持ったVMとパーサのライブラリなので「白地」というのが由来であって決して某19歳に見えない艦長率いる連邦の戦艦ではない。合わせてツールの方はWhitedevilにしようかなーとか一瞬考えたけどコマンド名としては長すぎるしrx78とかエイリアスするはめになるのでアルビノにした。VMとパーサが分離した結果どういうわけかBrainf*ck派生の言語をWhitespaceの命令セットに変換できるようになったので、「本来白くないものまで白くなっている」からアルビノなんです。

得られたもの。Rustがなかなか触っていて楽しいということが分かった。いまどきっぽいサブコマンド型のコマンドどうやって作ってるのかなーってのが分かった。WhitespaceもしくはBrainf*ckのトークンの内部表現を返すイテレータさえ実装できれば簡単にオレオレ言語が作れるライブラリが出来たので、今後さっと言語を作ってドヤリングしたいときは一瞬で出来るようになった。Brainf*ckの任意のプログラムからWhitespaceのコードが生成できるようになった。

失なったもの。時間。結構な無駄な時間。

ゴールデンウィークを利用して久々にろくでもないものができたので晒しておく。

「ごっこじゃないよ、兄ちゃん!」「才能の無駄遣いじゃなくて、無駄そのものだよ、お兄ちゃん。」

前のと何が違うの?

そもそもの発端についてはプログラミング言語「DT」という記事を、実際の言語仕様についてはREADMEでも読んでもらうとして、何で再実装したのかという話。

3年前に作ったのは「Whitespaceのトークンを置換した処理系をインタプリタとしてRubyで実装」したものなんだけど、何を思ったかコンパイルしてみたいと思い立ってしまい、んでそれ自体はLLVMが良く分かんなかったとか、llvmruby自体がプロジェクトとして大分アレな感じになってしまっていたとか、そういう事情で挫折してたんだけど、時は流れ、なんか見返してみたら普通にLLVMアセンブリ読み書きできるようになってたし、ruby-llvmがちゃんと動くようになってるみたいだし、ということで再実装してみることにした。

そんで作り直す過程で「Whitespaceの置換ってのもなんだか面白くないな」と思ってVMのアーキテクチャと言語仕様を見直すことにした(ぱっと見だと全然分かんないけど、旧DTと新DTの間に互換性は無い)。そのせいで実際にはチューリング完全じゃなくなってると思うし、そもそもちゃんと出来てるのかどうか怪しいものだけど、とりあえず「Hi!」って出力するのとフィボナッチ数を出すのは動いたので良しとする。

当初の目標だった「DTコードをネイティブのバイナリにまでコンパイルして高速に実行」ってのも実現して、DTインタプリタで250msくらいかかるfib.dtをコンパイルすると4msくらいで結果が出るようになった。似たようなRubyのコードだと大体100msくらいだったので素敵に速い。まぁ、コンパイル自体はLLVMとコンパイラに任せているので大して威張れるあれでもないのだけども。そして高速に動いたからなんだという話なのだけども。

ぶっちゃけDTよりLLVMアセンブリの方が遥かに記述力高いし書きやすい。当たり前のように構造体とか関数ポインタとか使えるのな。Cの関数呼べるし。最初Cで書いてたVMを途中でLLVMアセンブリ直接書くようにしたけど、大して変わらない。DTで書くくらいならLLVMアセンブリ書いた方がいいです。LLVMの無駄遣いタグを付けていただきたい。

DTの存在意義については疑問を差し挟む余地もなく皆無だと言っていいけど、その過程でレジスタマシンについて色々調べて、はー、こんぴゅーたーってこうやってうごいてるのかー、と勉強になったのでそれはそれで良かった。よくこんなの考えられるよなぁ。CPUとかコンパイラとか作ってる人達、本当に俺と同じ種類の生き物なのだろうか。凄い。

こんにちは、「それは一体誰得なんだ」でお馴染みの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でコンパイルしないと動かないんですけど。何でこんなコード入れてんだ俺。

プログラミング言語DTを作ったところ「いいからソースを晒せコラ」と言われたので、DT処理系のソースコードをgithubに上げてみた。

faultier's esoteric at master - GitHub

正確に言うと、DTの処理系と言うか、似非VMとそれで実行できる中間言語を吐くコンパイラ群のセット。その中にDTコンパイラとサンプルも入ってます。構成整理して無駄にgemspecを作ってあるので、

$ sudo gem install faultier-esoteric --source http://gems.github.com

とかやるとesotericがインストールできる…はず。なにしろgem作るのも初めてだしそれをgithubで自動ビルドさせるのも初めてなので、なんかしくじったかもしれない。てか、初めてのgemがこれか。それでいいのか、俺。まぁいいや。あ、できた。ちゃんとできてた。一応1.8.6-p287、1.8.7-p72、1.9.1-p0では動作確認済み。大したことやってないのでまぁ動くはず。

ちなみに、esotericに含まれてるコンパイラはWhitespaceとDTだけです。HRKは白紙に戻して、Nonowaと改称した上で言語仕様から考え直すため、Decadeはfaultierが某特撮番組を観てないので、ファンの人から怒られそうなため、今バージョンでのリリースは見送りました。今バージョン、てことは、esotericはバージョンアップするものらしい。何のために。誰が得するんだ。多分次のリリースあたりではBrainf*ckとかてってってーとかが取り込まれてて、その次のリリースあたりではNonowaが実装されたりしてるんだと思う。気が向いたら。飽きてなかったら。

あ、で、使い方だけど、インストールするとesm、whitespace、dtの三つのコマンドが使えるようになるので、

$ dt -v
esoteric 0.0.1, dt 0.0.1
$ dt hi.dt
Hi!

などとすると前の記事のコードが実行できたりする。これであなたも今日からDTer。

まさかそんな馬鹿いないとは思うけど、これ一緒にいじりたい、って人がいたら「べ、別にあなたのために手伝ってあげるんじゃないんだから、ただの興味よ、興味」とかメールで送ってください。別になにもしないけど。俺がニヤニヤします。…まぁ、githubにあるものなので好きにいじってください。あ、あとVMのこととか教えてくれる人がいたらすごく喜びます。

最近「Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~」を読んで、ついカッとなって言語処理系を作ってみた。それも、チューリング完全な、本物の(ry まぁ、Whitespaceのトークンを置き換えただけだけど。

一応、上記の本に習って、ソースコードをWhitespaceの命令セットを持つ中間言語にコンパイルしてVMで実行する形にしてみた。あとはパースする部分を適当に置き換えたコンパイラを作ればそれっぽいのが簡単に作れる。んで、いくつかネタ言語を作ってみたんだけど、その中でも一番しょーもないのがDT。DTでは次のようなソースを実行することができます。("Hi!\n"を出力する)

「○○くんて、もしかして童貞?」
「どどど童貞ちゃうわっ!どど童貞ちゃうわっ!どどど…童貞ちゃうわっ!」
「えー。その慌てっぷりが余計怪しなぁw」
「…どどどどど童貞ちゃうわっ!童貞ちゃうわっ!ど童貞ちゃうわっ!どど童貞ちゃうわっ!…童貞ちゃうわっ!」
「はいはい、わ、わかったってば。そんなにムキになんないでよ。」
「…どどどどど童貞ちゃうわっ!どどどど童貞ちゃうわっ!…童貞ちゃうわっ!」
「だから、わかったってば。もう言わないから。」
「…どどどどど童貞ちゃうわっ!ど童貞ちゃうわっ!ど…童貞ちゃうわっ!」
「いやその、なんか、ほんとごめんね。そんなに気にしてると思わなかったから、あの…」
「どど…」
「(ほんとキモいなぁ…)」
「…」

バレンタインだと言うのに一体何をしてるんですかね、僕は。ちなみに「ど」「童貞ちゃうわっ!」「…」以外の文字は全て無視するので、それらの順番と数さえあってればどんなコメントを挟んでも問題ない。コメント記法とかも特にない。ちなみに上記のコードから有効な部分を抜き出すとこんな感じ。一応読みやすくするために改行は入れてある。

どどど童貞ちゃうわっ!どど童貞ちゃうわっ!どどど…
童貞ちゃうわっ!…どど
どどど童貞ちゃうわっ!童貞ちゃうわっ!ど童貞ちゃうわっ!どど童貞ちゃうわっ!…
童貞ちゃうわっ!…どど
どどど童貞ちゃうわっ!どどどど童貞ちゃうわっ!…
童貞ちゃうわっ!…どど
どどど童貞ちゃうわっ!ど童貞ちゃうわっ!ど…
童貞ちゃうわっ!…どど
………

あと他に「アイドルが機嫌良さげに踊るののワ型言語HRK」とか、「9つの世界を巡る変身ヒーロー型言語Decade」とか作ったけど、どれも今のところただのWhitespaceなのでどんなソースになるかは想像つきますね。てことで割愛。もちろんどれも入出力や四則演算やジャンプ命令を備えてますので、フィボナッチ数列を無限に出力するとか余裕だし、その気になればきっとWebアプリぐらい書けますね。やりたかないけど。ていうかどれも書くことを全く考慮してないので、俺もうざくなって似非中間言語書いてそれを置換してソースコードにするっていう本末転倒なことしないと書けない。まぁそんなもん。

そろそろWhitespaceにも飽きたので、次は何しよう。Ruby分は十分補充できたので、Objective-CでVM書くとか、Parrotの上で処理系作るとかしようかな、とか。関数型言語もまだやってないので、それを実装するのもいいし、処理系をHaskelで書いてみるとかもしたい。しょーもない見た目の「まともな」言語仕様を考えるとかもしたいなー。やってみると結構面白い。ハマる。十分脳味噌ほぐれたらちゃんとした言語処理系のソースでも読んでみよう。

と、そんな現実逃避をして過した休日。うーん、充実した土日だった。え?何?何でDTなんか作ったのって?もしかしてfaultierって童…いいいいいやいやいや。どどどどど童貞ちゃうわっ!

注意

上の言語はほんとしょーもないですが、Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~の方はちゃんと面白い本なので誤解のなきよう。変な言語も色々紹介されてるし、Rubyでサクっと言語処理系つくるやりかたも書いてあるので、オススメです。

Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~
著者:原 悠
販売元:毎日コミュニケーションズ
発売日:2008-12-20
おすすめ度:4.0
クチコミを見る

追記

これ書いた後にクチコミを見たら弾さんがてってってーとかやってて、その記事のさらに元記事が結構なブクマ数だったりして、今頃それに気付いてなんか負けた気分。ぬぅ…。いいもん、ちゃんとののワさん言語完成させるから。もうなんかiPhoneで動くようにしちゃうから。配布できないけど(インタプリタは規約でAppStoreに登録できなかったはず)。

↑このページのトップヘ