🔻

Ruby のジョーク VM 命令を有効化してみる

に公開

あけましておめでとうございます。
年末は Ruby リリース 30 周年だったり、Ruby 4.0 がリリースされたりと Ruby 周辺が盛り上がっていたのを感じていました。

さて、そんな Ruby (MRI) の実装を眺めていたところ、 insns.def に次のようなものを見つけました。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/insns.def#L1693-L1711

insns.def は RubyVM の命令を定義しているファイルのようで、その中には bitbltanswer という2つのジョーク VM 命令が存在してるらしいことがわかりました。
今日はこの2つのジョーク VM 命令を実際に有効化してみた話をします。

前提として

  • 2026-01-07 時点の Ruby の master ブランチ (1abb609) を使用する
  • Ruby を手元でビルドできる環境が存在している

とします。

bitblt と answer を有効化する

insns.def を見ると、answer 命令は 42 が返ってくるようです。銀河ヒッチハイクガイドに出てくる「生命、宇宙、そして万物についての究極の疑問の答え」というやつですね。
bitblt 命令のほうは bit of bacon, lettuce and tomato という文字列が返ってくるようです。

bitblt と answer というキーワードで検索をかけると、compile.c にそれっぽい実装が見つかります。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/compile.c#L9454-L9470

これを見ると、メソッド名 (mid = method_id?) が the_answer_to_life_the_universe_and_everythingbitblt であれば、それぞれのジョーク VM 命令が使用されるように見えます。

この compile.c の実装を見る限りは、OPT_SUPPORT_JOKE が ON であればこの2つのジョーク VM 命令に関する実装がコンパイルされるようです。
gcc では -D オプションでマクロの定義を指定することができ、configure の optflags で gcc に渡すオプションを指定することができます。
そのため、以下のようなコマンドで Ruby をビルドすればジョーク VM 命令が有効になると思いました。

./configure optflags="-D OPT_SUPPORT_JOKE=1" && make -j ruby

しかしながら、これではうまく行きません。

In file included from ../compile.c:48:
../compile.c: In function 'compile_call':
./insns.inc:43:16: error: 'YARVINSN_bitblt' undeclared (first use in this function); did you mean 'YARVINSN_opt_lt'?
   43 | #define BIN(n) YARVINSN_##n
      |                ^~~~~~~~~
../compile.c:225:99: note: in expansion of macro 'BIN'
  225 |   ADD_ELEM((seq), (LINK_ELEMENT *) new_insn_body(iseq, nd_line(line_node), nd_node_id(line_node), BIN(insn), 0))
      |                                                                                                   ^~~
../compile.c:9463:13: note: in expansion of macro 'ADD_INSN'
 9463 |             ADD_INSN(ret, line_node, bitblt);

コンパイラが出したエラーをみると、bitblt 命令を使用するコードをコンパイルする際に、insns.incYARVINSN_bitblt がないみたいなことをと言っています。
insns.inc はビルド時に生成されるファイルのようで、VM 命令名が enum で定義されているようです。

insns.inc
/* BIN : Basic Instruction Name */
#define BIN(n) YARVINSN_##n

enum ruby_vminsn_type {
    BIN(nop),
    BIN(getlocal),
    BIN(setlocal),
// 中略
    BIN(zjit_opt_regexpmatch2),
#endif
    VM_INSTRUCTION_SIZE
};

生成された insns.inc を見ても確かに ruby_vminsn_type の中に BIN(bitblt) や、BIN(answer) が存在していません。

insns.inc の生成元を調べると tool/ruby_vm/views/insns.inc.erb であることがわかります。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/tool/ruby_vm/views/insns.inc.erb

この tools/ruby_vm/ 配下に存在している Ruby スクリプトが RubyVM 関連のソースコードを生成しているようです。
もっと詳しく調べてみると、insns_def.rbinsns.def ファイルを読み取っていることがわかります。
また、DEFINE_INSN_IF(SUPPORT_JOKE) に相当する箇所は opt に入れられ、RubyVM::VmOptsH[opt] の値を見て実際にソースコードを生成する対象の命令に加えているようです。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/tool/ruby_vm/loaders/insns_def.rb

RubyVM::VmOptsHvm_opts_h.rb で定義されていて、vm_opts.h から #define OPT_* * を抽出して VM 命令を生成する際のオプションにしているようです。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/tool/ruby_vm/loaders/vm_opts_h.rb

つまりは optflags で -D OPT_SUPPORT_JOKE=1 でコンパイル時にマクロを有効化しても vm_opts.h のファイルの中身が実際に書き換わるわけではないので RubyVM::VmOptsH に反映されず、insns.inc の内容は変わらなかったようです。
OPT_SUPPORT_JOKE を使用するには vm_opts.h を直接編集する必要がありました[1]

vm_opts.h
  /* misc */
  #ifndef OPT_SUPPORT_JOKE
- #define OPT_SUPPORT_JOKE             0
+ #define OPT_SUPPORT_JOKE             1
  #endif

これで無事に OPT_SUPPORT_JOKE を有効化した状態の Ruby をビルドすることができました。
早速試してみましょう。

./ruby -e 'puts the_answer_to_life_the_universe_and_everything'
-e:1:in '<main>': undefined local variable or method 'the_answer_to_life_the_universe_and_everything' for main (NameError)
-e: because using such Ruby (fatal)

ところが、the_answer_to_life_the_universe_and_everything メソッドがないとのエラーが出ます。
メソッドを定義していないので当然ではありますが、 compile.c の実装上はメソッド呼び出しの代わりにジョーク VM 命令のほうが実行されるようになるはずです。
--dump=insns で実際の VM 命令列を確認すると、answer は使用されておらず、通常のメソッド呼び出しになっているようです。

./ruby --dump=insns -e 'puts the_answer_to_life_the_universe_and_everything'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,11)>
0000 putself                                                          (   1)[Li]
0001 putself
0002 opt_send_without_block                 <calldata!mid:the_answer_to_life_the_universe_and_everything, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0004 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0006 leave

これは Ruby 3.4 からデフォルトパーサーが Prism になったことが原因のようです。
もともと compile.c には parse.y で生成された AST をコンパイルする実装が書かれているようで、このジョーク VM 命令は現状 parse.y でパースしたときにしか動かないようです。

--parser=parse.y でパーサーを parse.y 指定すると、ジョーク VM 命令が使用できるようになりました。

./ruby --parser=parse.y --dump=insns -e 'puts the_answer_to_life_the_universe_and_everything'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,51)>
0000 putself                                                          (   1 )[Li]
0001 answer
0002 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0004 leave

./ruby --parser=parse.y -e 'puts the_answer_to_life_the_universe_and_everything'
42

./ruby --parser=parse.y -e 'puts bitblt'
bit of bacon, lettuce and tomato

Prism でも bitblt と answer を使うようにする

ここまではもともと Ruby の実装に存在してたジョーク VM 命令を有効化して使用してみるという話でした。
先述の通り、ジョーク VM 命令をコンパイルするような実装が入ってるのは parse.y 用のコンパイラ側で、Prism 側には入っていません。

そこで次は Prism 側でもジョーク VM 命令がコンパイルされるようにします。

もともと compile.c 側で answer や bitblt 命令を出力しているのは compile_call 関数でした。
これは the_answer_to_life_the_universe_and_everythingbitblt を Ruby がパースした際にはメソッド呼び出しの AST ノード (厳密には NODE_VCALL ?) に相当し、それを担当しているのが compile_call 関数のようです。

したがって、Prism 側でも同じくメソッド呼び出しのノードをコンパイルする箇所で同様にジョークVM命令を出力するように修正すれば良さそうです。
Prism が構築した AST をコンパイルする実装があるのは prism_compile.c のようです。 prism_compile.cでメソッド呼び出しのノードをコンパイルする箇所は pm_compile_call_node あたりが近そうなので、 compile.c に存在している実装を参考に pm_compile_call_node に修正を加えます。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/prism_compile.c#L7340-L7349

prism_compile.c
   static inline void
   pm_compile_call_node(rb_iseq_t *iseq, const pm_call_node_t *node, LINK_ANCHOR *const ret, bool popped, pm_scope_node_t *scope_node)
   {
       ID method_id = pm_constant_id_lookup(scope_node, node->name);
  
       const pm_location_t *message_loc = &node->message_loc;
       if (message_loc->start == NULL) message_loc = &node->base.location;
  
       const pm_node_location_t location = PM_LOCATION_START_LOCATION(scope_node->parser, message_loc, node->base.node_id);
+ 
+  #if OPT_SUPPORT_JOKE
+      ID id_bitblt;
+      ID id_answer;
+ 
+      CONST_ID(id_bitblt, "bitblt");
+      CONST_ID(id_answer, "the_answer_to_life_the_universe_and_everything");
+ 
+      if (method_id == id_bitblt) {
+          PUSH_INSN(ret, location, bitblt);
+          return;
+      }
+      if (method_id == id_answer) {
+          PUSH_INSN(ret, location, answer);
+          return;
+      }
+  #endif
+ 
       const char *builtin_func;

これで Prism 使用下でもジョーク VM 命令が使用可能になりました。

 ./ruby --parser=prism --dump=insns -e 'puts the_answer_to_life_the_universe_and_everything'
 == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,51)>
 0000 putself                                                          (   1 )[Li]
 0001 answer
 0002 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
 0004 leave

 ./ruby --parser=prism -e 'puts the_answer_to_life_the_universe_and_everything'
 42

おまけ labelgoto を復活させる

compile.c の answer や bitblt の実装の下になにやら他にもジョーク実装が存在しています。

https://github.com/ruby/ruby/blob/1abb609d667d4e07fb30192ef9da376bb288e230/compile.c#L9471-L9503

よく見るとどうやら __label____goto__ を用いて、いわゆる goto 文のようなものを実現しているようです。
__goto____label__ も AST 上はメソッド呼び出し扱いになってそうなので、正確にはこれ単体は "文" ではないとは思いますが)
しかしながら、この実装は一部が削られていて不完全な状態なようです。
そこで、もともと存在していた実装を参考に goto 機能の復元をしてみます。

git のコミットを遡ると、該当箇所の実装が削られる前は以下のような実装になっていたようです。

https://github.com/yui-knk/ruby/blob/60567731051885acf38a3f91899f0d6d62d4898b/compile.c#L8934-L8954

どうやら削除された部分は、「__label____goto__ の引数の先頭のノードが NODE_LIT で、かつ Symbol であるとき」と思われる if 文が存在していたようです。
今は NODE_LIT は削除されて、Symbol リテラルかどうかは直接判断できるようになってるようなので、以下のように修正してみます。
(NODE_LIT の削除は数年前の RubyKaigi で発表のあった AST のリファクタリングをしたい話であったような気がします)

compile.c
// 略
               if (!labels_table) {
                   labels_table = st_init_numtable();
                   ISEQ_COMPILE_DATA(iseq)->labels_table = labels_table;
               }
+              if (nd_type_p(RNODE_LIST(get_nd_args(node))->nd_head, NODE_SYM)) {
+                  label_name = rb_node_sym_string_val(RNODE_LIST(get_nd_args(node))->nd_head);
+                  if (!st_lookup(labels_table, (st_data_t)label_name, &date)) {
+                      label = NEW_LABEL(nd_line(line_node));
+                      label->position = nd_line(line_node);
+                      st_insert(labels_table, (st_data_t)label_name, (st_data_t)data);
+                  }
+                  else {
+                      label = (LABEL *)data;
+                  }
+              }
+              else {
-              {
                   COMPILE_ERROR(ERROR_ARGS "invalid goto/label format");
                   return COMPILE_NG;
               }
// 略

これで Ruby で goto が使えるようになり、例えば以下のような Ruby スクリプトを実行すれば無限ループができるようになりました。

goto.rb
__label__ :label
puts 'Hello World!'
__goto__ :label
./ruby --parser=parse.y --dump=insns goto.rb
== disasm: #<ISeq:<main>@goto.rb:1 (1,0)-(3,15)>
0000 putself                                                          (   2 )[Li]
0001 putchilledstring                       "Hello World!"
0003 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0005 pop
0006 jump

./ruby --parser=parse.y goto.rb
Hello World!
Hello World!
Hello World!
...

まとめ

今回は Ruby のビルド時のオプションや実装をいじったりしてジョーク VM 命令の有効化をやってみました。
この手のジョーク実装やイースターエッグは他の OSS でもたまに見かけたりする[2]ので、探してみると面白いかもしれないですね。

脚注
  1. tool/ruby_vm/scripts/insns2vm.rb を見ると、このスクリプトに対して -D オプション指定をすると vm_opts.h の値を上書きができるが、unsupported になっているとのこと ↩︎

  2. 例えば ActiveSupport の Array 拡張には Array#forty_two が存在してたりする ↩︎

Discussion