mruby でもリファインメントを使いたい！！！

Rubyist の皆さんはリファインメント機能が好きですよね？僕も大好きです。

ですが残念なことに mruby では実装されておらず使えません。

まあ、実装されていないだけならば実装してみようと思い立ち、作業を始めてすでに３年ほどが経ったと思います。その作業途中で行き詰まって数ヶ月停滞することが何度もありましたが、それなりに完成してきたこともあってどこまで出来ているのかをまとめてみようと思いました。

なおすでにその途中の成果の一部は本当の背景を明かさずにそれっぽい理由をつけてプルリクを送ったところ、だいたい取り込まれています😋。

直近だと `OP_ASET` に関する「[Fixed a discrepancy in `OP_ASET` #5579](https://github.com/mruby/mruby/pull/5579)」がそれに当たります。これは `Symbol#to_proc` メソッドの再実装作業中に見つけたものです。キーワード引数が引き継がれないことがある問題に関しても、この作業途中で発見したことでした。


## 早速使ってみよう

使ってみようとは言っても、まだ本家には取り込まれていません。開発途上版であれば [僕の開発ブランチ](https://github.com/dearblue/mruby/tree/refinements) から試すことが出来ます。

リファインメント機能は `mrbgems/mruby-refinement` を取り込んで使うようになっています。つまり既定のままビルドすると「なにも出来ねーじゃん」状態となってしまうので気をつけて下さい。なので `build_config` へ明示的に追加する必要があります。

```ruby
MRuby::Build.new do
  toolchain
  gembox "default"
  gem core: "mruby-refinement"  ## 超大事なオマジナイ！！！
end
```

このように超大事なオマジナイを追加してビルドすれば、ESP32 上でもリファインメントが使えるようになります！

なお開発ブランチでは一時的に `mrbgems/mruby-refinement` を有効としているので、そのままビルドするだけです。

```console
% curl -L https://github.com/dearblue/mruby/archive/refs/heads/refinements.tar.gz | tar xf -
% cd mruby-refinements
% rake -m
```

ビルドできたらすぐにお試し出来ます！

```console
% cat <<RUBYCODE > test.rb
class Girl
  def age = 12
  def place = "小学校"
end

module Fake
  refine Girl do
    def age = 20
    def place = "ふくしの……大学？"
  end
end

girl = Girl.new
2.times do
  puts "#{girl.age}歳なんですけど！　#{girl.place}に通ってるんですけど！"
  using Fake
end
RUBYCODE

% bin/mruby test.rb
```


### 欠点

RAM をいっぱい消費するようになります。すぐ上で ESP32 でも使えると書きましたが、RAM が 1 MiB 以下の機器で使うことはおすすめできません。それにリファインメントが有効な範囲ではメソッド探索が遅くなる上に、メソッドキャッシュ処理も迂回されます。


## メソッドを定義できるようにしよう (課題あり)

メソッドを登録するのであれば `struct RClass` を使うようにすればいいはずです。`MRB_TT_MODULE` と区別するために `MRB_TT_REFINEMENT` を追加したのは作業を始めた最初期の頃でした。

リファインメントオブジェクトが `include` / `extend` 出来ることを知らなかったので、まあしゃあない。そのうち `MRB_TT_MODULE` と `struct RClass::flags |= MRB_MODULE_REFINEMENT` みたいな形に変えなきゃ。

```ruby
module M
  R = refine Object do
  end

  module R  # => M::R is not a module (TypeError)
            # CRuby は問題なし
  end
end
```


## リファインメント情報を保存しよう

`using` メソッドで有効化させますが、どうやってその情報を保存させるかは悩みどころでした。

リファインメントはスコープ途中で動的に有効となります。その仕組み上、現在呼び出し中のスコープでリファインメント情報を更新する必要があります。どうしても呼び出し情報 (`mrb_callinfo`) にメンバを増やす必要がありますが、こちらはまだいいです。

メソッドとして登録されることを考えるとブロック (`struct RProc`) に保存する必要がありますが、追加する余地がありません。

なので、`struct RProc::body` メンバに目をつけて、これを拡張しました。リファインメントが有効な時は `struct RProc::body.extra` メンバを経由して、ここに `irep` と `attached_refinements` を保持するための構造体を新設します。

これまでは `struct RProc::body.irep` メンバを直接参照することがありましたが、`body.direct_irep` という名前に変更しました。`struct RProc::body.irep` を直接参照するのではなく、`MRB_PROC_IREP()` マクロを使うようになります。

こうすることで `struct RProc::body.extra` を想定していない古いソースコードはコンパイルエラーになります。互換性は犠牲になりました。

```c
struct RProc {
  MRB_OBJECT_HEADER;
  union {
    const mrb_irep *irep_direct;    // irep から変更❕
    mrb_func_t func;
#ifdef MRB_USE_REFINEMENT           // 🆕❕
    struct mrb_proc_extra *extra;   // 🆕❕
#endif                              // 🆕❕
  } body;
  const struct RProc *upper;
  union {
    struct RClass *target_class;
    struct REnv *env;
  } e;
};
```

リファインメントを実現するためのデータ構造を整理・把握するために図を作りました。~~作業を投げ出した後で再開する時にとても有用です。~~

![refinements.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42844/636cb1ad-b59b-42cd-b98b-2e5beaae0b06.png)


## コールスタックの増加を抑えよう

有効化したリファインメント情報を保持する 1 ワード (`struct RArray *`) を `mrb_callinfo` へと追加したことを前述しました。

作業を始めた時期が mruby-1.4.1 の頃だとすると、この時点で `sizeof(mrb_callinfo)` は 32 ビット CPU で 12 ワードです。ここでさらに 1 ワードを追加することは躊躇してしまいますね。mruby-2.1.2 の時点では 10 ワードですが、この頃には減らす算段がついていました。

ある時 mruby のリポジトリへファイバーに関する問題が報告され、いい切っ掛けと思って送ったプルリクエストが「[Reorganize `mrb_callinfo` #5272](https://github.com/mruby/mruby/pull/5272)」です。この変更によって、6 ワードにまとめることができました。これなら 1 ワードくらい増えても文句が出にくいことでしょう。なお `MRB_USE_REFINEMENT` が未定義であれば増やさずに 6 ワードを維持します。


## 子スコープ・ブロックへリファインメント情報を伝搬させよう

新しいブロックやスコープが生成された場合、リファインメント情報を継承します。現在の呼び出し情報 (`mrb_callinfo *ci = mrb->c->ci`) からリファインメント情報を取得し、`mrb_proc_new()` (あるいは `mrb_closure_new()`) にそのまま渡します。このリファインメント情報は、リファインメントが定義されている配列オブジェクトである `struct RArray` へのポインタです。

ここでモジュールやクラスのスコープでリファインメントの適用範囲が変わってくることを思い出して下さい。配列オブジェクトを共有することは望ましくないと思うかもしれません。

親スコープも子スコープも共有できるうちは共有して、`#using` メソッドが呼ばれたらその中でコピーと追加を行うようにしました。そのため `#using` が連続する場合、一時的に配列オブジェクトのコピーが多発することになります。

このあたりも多くの RAM を必要とする理由になります。


## `refine` ブロックの中で自身のリファインメントを影響させよう

`#refine` メソッドに与えたブロックの内部では、それ自身で定義したメソッドも呼び出せる仕様になっています。つまり暗黙のうちに `#using` メソッドが限定的に使われているわけですね。

mruby では Ruby 空間で呼び出し情報 (`mrb_callinfo *ci`) を細工出来ないので、C 空間で `ci` をいじってから渡されたブロックに制御を渡しています。

ところで mruby だとメソッドの可視性が常に `public` なので、直接 `Kernel.refine` を呼んでリファインメントを定義できてしまいますよね。

どうにかしようかなと思いましたが、Ruby-3.1 preview1 で

```ruby
class Module
  public :refine
end

Kernel.refine Object do
  def ほげ
  end
end
```

というコードで確認したら、まあいいかという気になってそのままです。


## `refine` メソッドの戻り値をリファインメントオブジェクトにしよう (課題あり)

`Module#refine` メソッドの戻り値はリファインメントオブジェクトです。引数として与えたブロックの戻り値ではありません。

すでに普通のブロック呼び出しを行えないことは述べました。なのでさらに特殊な呼び出し方を行っています。

`mrb_irep_exec()` による末尾呼び出しを用いますが、さらに手を突っ込んで予め `return self` するだけのブロックで `refine` メソッドの呼び出しをすり替え、その上にブロック引数を呼び出すようにしています。こうすることでブロックの戻り値に関係なくリファインメントオブジェクトを返すようになっています。これは以前 `Binding` を実現するときに取ったフックブロックのような手法です。

ただ、これを書いている時に `mrb->c->ci->stack` を一つずらすだけでリファインメントオブジェクトを返せるなと思い至りました。そうすれば `mrb_cipush()` を露出させる必要もなくなるし。


## モジュールでもリファインメントを使えるようにしよう (課題あり？)

Ruby-2.4 からはモジュールに対してもリファインメント機能が有効になっています。mruby でもモジュールに対してリファインメントを対応させました。

特に悩んだのは `prepend` と `super` を組み合わせた場合のメソッド探索です。作業をちょっとして数ヶ月投げ出してを何度か繰り返し、結局満足行く出来になるまで１年を越えることになりました。

根本的な原因は継承やミックスインの実現方法を分かった気になっていたことです。そのため、途中でどのようにポインタが継っているのかを `mrb_prepend_module()` の動作から確認することにしました。

![class.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42844/b0a4160d-ba9b-a049-4c08-49eea4b9190e.png)

これでもまだ試行錯誤を繰り返しましたが、どうにか満足の行く結果を得ることが出来るようになりました。

そのあと忘れていた CRuby の挙動と確認してみたら……あれ、結果が違う？

```ruby:refine-super.rb
module A
end

module M
  refine A do
    def downcase
      "<<#{super}>>"
    end
  end
end

class String
  prepend A
end

using M
p "ABCDEFG".downcase
```

```console
% bin/mruby refine-super.rb
"<<abcdefg>>"

% ruby31 refine-super.rb
refine-super.rb:7:in `downcase': super: no superclass method `downcase' for "ABCDEFG":String (NoMethodError)
Did you mean?  downcase!
        from refine-super.rb:17:in `<main>'
```


## gem に分離しよう

最近になってリファインメント機能の大部分をまとめてコアから分離できることに気が付きました。ソースコードの見通しも良くなるし、`build_config` に `conf.defines << "MRB_USE_REFINEMENT"` とするよりは `conf.gem core: "mruby-refinement"` とした方が好ましいだろうという判断もあります。

なので、リファインメント関連の以下のメソッドは gem で定義されます。

  - `main#using`
  - `Module#using`
  - `Module#refine`
  - `Module.used_modules`

そのため mruby のコアには、リファインメントを実装するためにどうしても必要となる拡張部分のみが残ることとなりました。その拡張部分は多くが `#ifdef MRB_USE_REFINEMENT 〜 #endif` で囲まれているので、リファインメントを使わなければバイナリサイズの増大は最小限に抑えられているはずです。正確な比較はそのうち……。


## `Symbol#to_proc` をリファインメントに対応しよう

Ruby ではシンボルをブロックとして与えることが出来ます。

```ruby
p [1, 2, 3].reduce(&:+)
```

この時の `+` メソッドはリファインメント対象となります。

mruby において `Symbol#to_proc` メソッドは `mrblib/symbol.rb` ファイルで定義されていますが、ファイル拡張子から分かるように Ruby で書かれています。ここで生成されるラムダオブジェクトはリファインメントの影響を受けるブロック呼び出しが行なえません。

```ruby
# 引用元: https://github.com/mruby/mruby/blob/39191cd9fadfea4940298cbcac2533597f76aa71/mrblib/symbol.rb#L2-L6

  def to_proc
    ->(obj,*args,**opts,&block) do
      obj.__send__(self, *args, **opts, &block)
    end
  end
```

リファインメントを有効にした状態のラムダオブジェクトを返せばいいので、`Symbol#to_proc` は C で再実装し、生成されるラムダオブジェクトに細工をすれば目的は達成できます。このラムダオブジェクトは Ruby で書いたものを `bin/mrbc` でコンパイルしたバイトコードから構成されています。

```
000 OP_ENTER     1:0:1:0:0:0:1 (0x41001)
004 OP_MOVE      R4  R1         ; R1:obj
007 OP_LOADSELF  R5
009 OP_ARRAY     R5  R5  1
013 OP_MOVE      R6  R2         ; R2:args
016 OP_ARYCAT    R5  R6
019 OP_MOVE      R6  R3         ; R3:block
022 OP_SENDB     R4  :__send__  n=*|k=* (0xff)
026 OP_RETURN    R4
```

だけどスプラッタ引数のための配列オブジェクトを再利用できないかな？ ということで新しい命令を加えて、ハンドアセンブルします。

```
000 OP_ENTER     0:0:1:0:0:1:1 (0x1003)
004 OP_JMPEMPTY  R1  +17 ; to 025
008 OP_AREF      R4  R1  0
012 OP_ASET      R0  R1  0
016 OP_MOVE      R0  R4
019 OP_SSENDB    R0  :__send__  n=*|k=* (0xff)
023 OP_RETURN    R0

025 OP_GETCONST  R1  ArgumentError
027 OP_STRING    R2  L[0]  ; "no receiver given"
030 OP_RAISEEXC  R1  (R2)
032 OP_RETURN    R0  ; not reached
```

ここで2つの新命令を追加しました。レジスタのオブジェクトが空 (`obj.empty?`) であれば分岐する `OP_JMPEMPTY` 命令、そして指示された例外を発生させる `OP_RAISEEXC` 命令です。

最初にちょろっと紹介した `OP_ASET` 命令を使っているのがわかるでしょうか？

この時はいいアイディアだと思いましたが、間もなく気が付きます。`__send__` メソッドまでリファインメントの餌食となってしまうことに。

それで結局は次のようになりました。

```
000 OP_ENTER     1:0:1:0:0:1:1 (0x41003)
004 OP_FORWARD   R1.__send__(R0, *R2, **R3, &R4)
005 OP_RETURN    R0
```

`OP_FORWARD` 命令を追加していますが、とてもシンプルです。`Symbol#to_proc` に関してはこれでよさそう。


## `Proc` のリファインメント情報を更新しよう

```ruby
2.times {
  a.namamugi
  using Namagome
}
```

こんなコードがあった場合、一度目は普通の `#namamugi` メソッドが呼ばれます。二度目はリファインメントが効いた状態の `#namamugi` メソッドが呼ばれます。

`Proc` オブジェクトが ROM に置かれていたら対応できませんが、書き換え可能であれば対応するようにしました。


## 文字列展開される時の暗黙の変換に対応しよう (課題あり)

Ruby では文字列の中でオブジェクトが暗黙的に `#to_s` されますが、これを行う時の処理をどうするか脳内で審議中です。とりあえず `OP_SEND` を `OP_STRCAT` の前に追加するようにしていますが、正直望ましくないよなあということでこの案は没になります。

```
# "1#{2}3"

000 OP_STRING   R1  "1"
003 OP_LOADI_2  R2
005 OP_SEND     R2  :to_s  0  ; 本当はいらない
009 OP_STRCAT   R1
011 OP_STRING   R2  "3"
014 OP_STRCAT   R1            ; => "123"
```

その後、`OP_SEND` 命令の代わりに `OP_TOSTRING` 命令を追加しましたが、やっぱり望ましくありません。

現在は `OP_STRCAT` で文字列以外 (`mrb_type(obj) != MRB_TT_STRING`) が与えられたら、VM の中から `#to_s` を強制するようなブロック呼び出しを行い、再度 `OP_STRCAT` を実行するようにしようかなと思案中です。この時変換しても `String` インスタンスでなければ `OP_STRCAT` で永久ループになるため、型の確認を行う新規命令も必要そうです。

```
000 OP_SEND      R0  :to_s  0
004 OP_GETCONST  R1  :String
007 OP_CHECK     R0  (R1)     ; 新規命令
009 OP_RETURN    R0
```

でも `String` 定数を入れ替えられたらすぐに無限ループするし、代わりに `enum mrb_vtype` を使うと mruby/c のような第三者実装系で問題になりそうだしどうしたもんだか……。専用命令にするしかなさそう？


## `#const_missing` メソッドにもリファインメントを適用しよう (課題・放置中)

mruby 仮想機械の中では定数を取得する時に `OP_GETCONST` あるいは `OP_GETMCNST` という命令が使われます。この命令は `mrb_vm_const_get()` または `mrb_const_get()` という関数を呼び出すだけです。その関数の中で定数が見当たらなければ、`mrb_funcall()` を通して `#const_missing` メソッドが呼び出されます。

これは別のブランチで `mrb_vm_exec()` 関数の再呼び出しを避ける目的で書き換え作業中となっています。機能的には問題がない状態ですが、全体的なパフォーマンスが5%〜10%落ちるという問題があって、現在絶賛放置中です…………。

&lt;閑話&gt;

`mrb_vm_exec()` 関数の再呼び出しを避ける関連でもう一つ。

`case obj; when *ary; ...; end` みたいなコードがある時、`when *ary` の部分は `#__case_eqq` メソッドが使われます。

```ruby
OP_SEND  R2  :__case_eqq  n=1 (0x01)
```

この `#__case_eqq` メソッドの実体である `src/kernel.c:mrb_obj_ceqq()` を見てみると、内部で `mrb_funcall()` を経由した `mrb_vm_exec()` への再呼び出しが確認できます。これを専用命令 `OP_CASE_EQQ` (仮) で仮想機械上のメソッド呼び出しに置き換えてやれば、`mrb_vm_exec()` への再呼び出しを抑制できるかもしれません。というか、これを書いていたら、ここもリファインメントが適用されてしまうことに気が付きました。

リファインメントの効かない特殊な `OP_SEND` が必要となるかも……。

&lt;/閑話&gt;


## その他の課題を確認しよう

まだ全く手を付けていないものもまだあります。

  - `#method` メソッド
  - `#methods` メソッド
  - `#respond_to?` メソッド

他にもあるけど、それは追々 `@pink_bangbi` さんのブログを確認しながらにします。

  - https://secret-garden.hatenablog.com/entry/2018/12/03/231925
  - https://secret-garden.hatenablog.com/entry/2018/12/25/235407

それと公式によるリファインメントの仕様書も改めて確認する必要があります。
https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/RefinementsSpec

一番難しい課題はテストを書くことですね。


## で、いつ来るの？

この先１年の間にはプルリクエストを送れるようにしたいなあ……。