Ruby版IOCCCであるところのTRICK 2015というコンテストで審査員賞"Matz Lisp award"をいただきました。
受賞コードを抜粋するとこんな感じ。
(DEFINE (FACT N) . ( (IF (EQ? N . (1)) . ( 1 [* N . ((FACT (- N . (1))))])))) (DISPLAY (FACT 6))
よく訓練されたRubyistにとってはただのRubyのコードにしか見えないかもしれませんが、 一応これはSchemeとしても有効なコードになっていて、実行すると6!を計算して出力します。 また、任意のプログラムを外部から与えることができるようにもしていて、 その一例としてSICPに出てくる超循環評価機をポーティングしています(metacircular.rb)。
ということで、今回の作品は一言でいうと「PolyglotなSchemeサブセットのインタプリタ」でした。
Schemeにおいて、シンボルが大文字小文字を区別しないのはR5RSまで、[]を()と同様に扱うようになったのはR6RSからなので より正確には「R5RSサブセット+一部R6RS拡張」になります。 シンボルを大文字にしたのはなるべくオリジナルに忠実に超循環評価機のポーティングをしようとしたため*1ですが、 原理的には小文字にすることも可能です。純粋なR6RSサブセットをお求めの方のために、小文字バージョンも用意しておきました(entry-lowercase.rb)。
実装のポイントはobj.callのシンタックスシュガーobj.()の活用です。remarksから引用します。
リスト((A) B)はこのままだとRubyでsyntax errorになりますが、 ドット対表記((A) . (B))にすると((A).call(B))と解釈されパーサを通るようになります。
これを利用してRuby/Schemeの両方で有効なコードを用意し、 実行時にmethod_missing/const_missingなどを使いながら構文木を組み立ててSchemeプログラムとして評価しています。
基本的にはこれですべてですが、Schemeにおける`FOO'と`(FOO)'はRubyのコードとしてパースすると違いがないので、後者は(FOO . ()))と書くことにしようといったルールを決めていかないといけないのがちょっと大変でした。
また、Refinementsも使っていて(metacircular.rb#L7)、RubyコードとSchemeコードが無理なく共存できるようになっています。ただ、Object.const_missingはRefinementsが有効にならないという仕様があり*2、これだけはグローバルに影響が出てしまうのが心残りです。
実はここまで書いたその日の夜にmethod_missingでRefinementsが有効になるのはバグだとの判断がありプログラムが動かないようになっていました*3。 確かにRefinementsの仕様をよく見てみるとIndirect method accessesではRefinementsを有効化しないという記載があるためこの変更には納得するところで、図らずもTRICKの理念の一つである「Rubyの仕様を安定化する」ことに貢献できた感があります。
なお、GitHub上のコードはRefinementsを使わずにモンキーパッチをあてるように修正したので、少なくとも当面は動いてくれるはずです。