Rubyの全バージョンで動くQuine

このプログラムは、Ruby 0.49(1994年リリース)からRuby 3.2.1(今月リリース)まで、現在確認されているすべてのCRubyで動作するQuineです。

          eval($s=("t='eval($s=('+d=34.chr;s=3
        2.chr+$s*i=8;v=$VERSION||eval('begin;v=V
      ERSION;rescue;v||RUBY_VERSION;end');f=('?'*8
    +'A|'+'?'*20+'G?c'+'?'*15+'A@CXx@~@_`OpGxCxp@~pO
  xS|O~G?c?q?xC`AP|q?x_|C_xC_xO@H@cG?G?qA|_|_`GCpOxC|H
NFccqq@`_|OF@`?q?x_@x_x_`GB`O``O~G?C@qCxCxP@D@|G~C?pO|C?
  pO|C?AP|A~HNN`ccxC|Q@L@B"+"GpGpc@p?x_`GB`???_@FO|OB@
     xC|P`@?c?q?HPx@~@_`G@`????@L^`?q?x?xq@|_|O~GC`
        xA~@_@GBD').unpack('c*');w=4+v.length*u=
           15;r=10.chr;j=0;while-24+w*u>i=1+i
              ;x=i%w;x>0||t=t+d+'+'+r+d;k=
                 i/w%12>2&&x%u>3&&x%u+i
                    /w*11-34+('-._'.
                       index(c=v[
                         x/u,1]
                )||c.hex        +3)*99|
               |0;    k=f     [k/6   ][k%
                       6];    t=t     +s[
                      k*j     =k+     j,1
                 ]end;pr      int     (t+
                      d+'     ).s     pli
                       t.j    oin     [0,
               609    ])#     Y.E.   '+r)
                ").split        .join)#

プログラミング言語Ruby 30周年記念イベントでLT発表したものです。コードはGitHubに置きました。

github.com

動作方法

全バージョンで実行するには、rubylang/all-rubyというdocker imageを使ってください。

$ wget https://raw.githubusercontent.com/mame/all-ruby-quine/main/all-ruby-quine.rb

$ docker run --rm -ti -v `pwd`:/src rubylang/all-ruby

./bin/ruby-0.49 /src/all-ruby-quine.rb とすると、ruby 0.49で実行できます。

# ./bin/ruby-0.49 /src/all-ruby-quine.rb
eval($s=("t='eval($s=('+d=34.chr;s=32.chr+$s*i=8;v=$VERSION||eval"+
"('begin;v=VERSION;rescue;v||RUBY_VERSION;end');f=('?'*8+'A|'+'?'"+
"*20+'G?c'+'?'*15+'A@CXx@~@_`OpGxCxp@~pOxS|O~G?c?q?xC`AP|q?x_|C_x"+
"C_xO@H       @cG?G?qA|_|_`GCpOxC|HNFcc     qq@`_|O         F@`?q"+
"?x_@    x_x    _`GB`O``O~G?C@qCxCxP@D      @|G~C?    pO|    C?pO"+
"|C?A   P|A~H   NN`ccxC|Q@L@BGpGpc@p?   x   _`GB`?   ??_@F   O|OB"+
"@xC|   P`@?c   ?q?HPx@~@_`G@`????@L   ^`   ?q?x?x    q@|_   |O~G"+
"C`xA   ~@_@G   BD').unpack('c*');w   =4+   v.lengt          h*u="+
"15;r   =10.c   hr;j=0;while-24+w*u   >i=   1+i;x=i%w;x>0|   |t=t"+
"+d+'   +'+r+   d;k=i/w%12>2&&x%u>3           &&x%u+i/w*11   -34+"+
"('-.    _'.    index(c=   v[x/u,1])||c.h   ex+3)*   99||    0;k="+
"f[k/6]       [k%6];t=t+   s[k*j=k+j,1]en   d;print         (t+d+"+
"').split.join[0,609])#Y.E.'+r)t='eval($s=('+d=34.chr;s=32.chr+$s"+
"*i=8;v=$VERSION||eval('begin;v=VERSION;rescue;v||RUBY_VERSION;en"+
"d');f=('?'*8+'A|'+'?'*20+'G?c'+'?'*15+'A").split.join[0,609])#Y.E.

同様に、./bin/ruby-3.2.1 /src/all-ruby-quine.rb とすればruby 3.2.1で実行できます。

# ./bin/ruby-3.2.1 /src/all-ruby-quine.rb
eval($s=("t='eval($s=('+d=34.chr;s=32.chr+$s*i=8;v=$VERSION||eval('begin;v=VERSI"+
"ON;rescue;v||RUBY_VERSION;end');f=('?'*8+'A|'+'?'*20+'G?c'+'?'*15+'A@CXx@~@_`Op"+
"GxCxp@~pOxS|O~G?c?q?xC`AP|q?x_|C_xC_xO@H@cG?G?qA|_|_`GCpOxC|HNFccqq@`_|OF@`?q?x"+
"_@x_x        _`GB`O``O~G?C@qCxCxP@D         @|G~C?pO|C?pO|C?AP|A~HN    N`ccxC|Q"+
"@L@B   GpGp   c@p?x_`GB`???_@FO|OB   @xC|P   `@?c?q?HPx@~@_`G@`???     ?@L^`?q?"+
"x?xq@|_|O~GC   `xA~@_@GBD').unpack('c*');w   =4+v.length*u=15;r=1  0   .chr;j=0"+
";while-24+w   *u>i=1+i;x=i%w;x>0||t=t+d+'+   '+r+d;k=i/w%12>2&&x%u>3   &&x%u+i/"+
"w*11-3       4+('-._'.index(c=v[x/u,1])|    |c.hex+3)*99||0;k=f[k/6]   [k%6];t="+
"t+s[k*j=k+j   ,1]end;print(t+d+').spli    t.join[0,609])#Y.E.'+r)t='   eval($s="+
"('+d=34.chr;   s=32.chr+$s*i=8;v=$VE    RSION||eval('begin;v=VERSION   ;rescue;"+
"v||R   UBY_   VERSION;e   nd');f=(    '?'*8+'A|'+'?'*   20+'G?c'+'?'   *15+'A@C"+
"Xx@~@        _`OpGxCxp@   ~pOxS|O~           G?c?q?xC   `AP|q?x_|         C_xC_"+
"xO@H@cG?G?qA|_|_`GCpOxC|HNFccqq@`_|OF@`?q?x_@x_x_`GB`O``O~G?C@qCxCxP@D@|G~C?pO|"+
"C?pO|C?AP|A~HNN`ccxC|Q@L@BGpGpc@p?x_`GB`???_@FO|OB@xC|P`@?c?q?HPx@~@_`G@`????@L"+
"^`?q?x?xq@|_|O~GC`xA~@_@GBD').unpack('c*');w=4+v.length").split.join[0,609])#Y.E.

./bin/ruby-3.2.0 /src/all-ruby-quine.rb | ./bin/ruby-0.49 で、ruby 3.2.0の出力をruby 0.49で動かせます。逆も可。

全バージョンで動かしたかったら ./all-ruby all-ruby-quine.rb としてください。

Rubyの全バージョンで動くプログラムの作り方

たぶん誰の役にも立ちませんが、このQuineを書くのに得られた知見をまとめておきます。

ブロックは使用不可

ruby 0.49のブロックは、do ary.each using x ... end という記法だったようです。現代で見る ary.each {|x| ... }ary.each do |x| ... end のようなブロックはありません。旧式の文法は現代のRubyではsyntax errorになるので、全バージョンで動かすにはブロックは使えません。while文などでがんばりましょう。

x += 1は使用不可

ruby 0.49には x += 1x ||= 1 のような構文がありません。x = x + 1 などと書き下します。

三項演算子は使用不可

ruby 0.49には cond ? a : b がまだありません。if文はありますが、ちょっと長いので cond && a || b などとするとおしゃれで しょう。

大きい文字列リテラルは使用不可

ruby 0.49で大きめ(700バイトくらい)の文字列リテラルを作るとSEGVするようでした。文字列の連結を使って回避します。ちなみに文字列の連結で動くということは、全バージョンでGCがそこそこ安定して動いているってことなので、結構すごいことです。

%-記法のリテラルや式展開は使用不可

この手のQuineで非常に便利な %(...) という文字列リテラルは、ruby 0.49では未実装です。String#splitArray#joinがあるので、コードのアスキーアート化は eval(s="...メインのコード...".split.join) とすれば可能です。

あと、"#{expr}"もないので注意。がんばって文字列連結しましょう。

evalの中でメソッド定義は使用不可

ruby 1.3.1-990324 では eval("def foo; end") などとすると nested method definition という例外になります。おそらくバグなんですが、いずれにせよ全バージョンで動かすためには使えません。

evalの中でコメントは使用不可

ruby 1.1d0 で eval("1#") などとするとSEGVします。間違いなくバグですが、コメントを使うのは避けましょう。

evalでローカル変数を参照するのは不可

再現条件がやや微妙なのですが、ruby 0.51など初期バージョンでは次のコードでSEGVします。

s = "hello"; eval("print(t = s)")

evalで外のローカル変数を読み出すところにバグがあるようでした。グローバル変数を使うと安定して動きました。

$s = "hello"; eval("print(t = $s)")

str[idx]に注意

Ruby 1.8まで、str[idx]はidx番目のバイトを整数で返していましたが、1.9からは1文字の文字列に変わっています。なのでstr[idx] は使わないのが無難です。どうしても使いたかったら、たとえば次のように書けば良いでしょう。

ch="ABC"[1]; "@"[0] == 64 && ch=[ch].pack("C*")

これで全バージョンで ch == "B" になります。ポイントは、"@"[0] が整数かどうかを見て分岐するところです(?@ == 64 でもよい)。型がなくてよかったですね。

利用可能な組み込みメソッドに注意

当然ですが、昔のRubyは今ほど組み込みメソッドが充実していないので、何が利用可能かを慎重に試す必要があります。とはいえ、ruby 0.49の時点で意外と多いです。現代の組み込みメソッドの半分以上はすでにあるんじゃないかな。

どんなメソッドがあるかは、ruby 0.49に同梱されているspecというファイルが便利です。かつて「Rubyにはドキュメントがない」と言われていたのはなんだったのか。

Rubyのバージョン番号を出力するプログラム

all-ruby-quineの肝はインタプリタのバージョン番号を取得するところなのですが、これが結構 hacky でした。その部分だけ取り出した次のコードが、各Rubyバージョンでどのように解釈されるか説明しておきます。

print($VERSION||eval('begin;v=VERSION;rescue;v||RUBY_VERSION;end'))

このコードが各バージョンでどのように解釈されるかを説明しておきます。

0.49 .. 0.65

これらのバージョンでは $VERSION が定義されています。よって、$VERSION || ... の後半は評価されず、そのまま $VERSION が返ります。後半でeval を使っているのは、これらのバージョンでは begin; ...; end の構文がまだ存在せず、そのまま書いたら syntax error になるからです。

0.69 .. 0.76

ここが一番おもしろいです。これらのバージョンでは $VERSION が定義されておらず、定数のVERSIONが定義されています。後述する1.9.0からはRUBY_VERSIONにリネームされるのですが、RUBY_VERSIONを参照すると例外になってしまうので、少し工夫が必要です。次のようにしました。

begin
  v = VERSION
  rescue
  v || RUBY_VERSION
end

rescue のインデントがおかしいのは意図的です。というのも、これらのバージョンでは例外捕捉キーワードは resque であり、rescue はただのメソッド呼び出しと解釈されます。「なら undefined method エラーになるのでは?」と思うかもしれませんが、幸いなことにこれらのバージョンでは「未定義メソッド呼び出しは黙って nil を返す」というパワーのある仕様です。よって、変数 vVERSION の中身が代入されたあと、v || RUBY_VERSION の後半は評価がショートカットされるので、無事 VERSION が返されます。

0.95 .. 1.8.7-p374

これらのバージョンでは VERSION が定義されています。0.95で resquerescue に変わったので、素直に v = VERSION だけ評価されて値を返します。

1.9.0 ..

VERSION 定数が削除されたので、v = VERSIONNameError を投げます。しかし rescue によって補足されるので、RUBY_VERSION が評価されて値を返します。

感想

なにより驚いたのは、ruby 0.49の時点でかなりRubyになっていることです。現代のRubyの機能の半分以上はすでにありそう。

そして、そこからほとんど変わってないのもすごい。このQuine程度に非自明なプログラムが書けるくらいに、本質的な非互換がない。やりはじめたときは、ここまでできるとは思ってませんでした。

Rubyは未完成だったので開発協力者がたくさん現れて、それがOSSとしての成功につながった」のようにmatzが語っているのはわりと有名ですが、そうはいっても「最初の時点でかなり完成していること、そしてそこからブレないこと」も成功するOSSの秘訣なのではないかという気がします。

それから、初期も含めて各リリースの品質が非常に高い。バグ回避のテクニックをたくさん書いたので言ってることと違うと思うかもしれませんが、言語処理系って多くの人が実戦で使ってはじめてまともになるものなんですよね。信じられない人はquine-relayを作ってみるといいです。世のマイナー言語処理系たちがいかにふつうのコードでもすぐSEGVするとか、致命的に機能が足りないとか、そもそも起動もしないとかがわかります。それを考えると、ユーザがほとんどいなかった初期でもRubyの各リリースの品質がここまで高いのは驚異的です。

ということで、matzはすごい!ということを体感できる遊びでした。だれかPythonとか他の言語でもやるといいと思います。

蛇足

ruby 0.49以前のコードが発掘されませんように。