mruby で特殊変数 `$!` を使えるようにするには？ (脳内妄想・アイディア段階)

mruby には特殊変数である `$!` が実装されていません。
また `rescue` / `ensure` 中に C 空間からエラーを直接取得する、CRuby にある `rb_errinfo()` 関数のようなものもありません。

`mrb->exc` が利用できるだろうと考えたこともありますが、`rescue` / `ensure` ブロックの中では仮想機械のスタックやマシンスタックに一時保存されて `mrb->exc` は空っぽになってしまいます。

どのようにしたら実装できるかについて頭の中で考えていることを書き留めてみようと思います。
なおあくまでも脳内妄想であり、実際に試したわけでも現在取り組んでいるわけでもないことを明言しておきます。


## やること① 特殊変数の仕組みを使えるようにする

まず、mruby には特殊変数のための仕組みそのものが実装されていません。
どういう事かと言うと、`$!` どころか `$0` だったり `$~` だったりがまるっきり機能しません。

`bin/mruby` でも `$0` が使えるでしょと思ったそこのあなた、mruby だとそれはただの大域変数です。
このことは `$0` に文字列を代入してみても、`ps` コマンドでの表示が変更されないことを意味しています。

mruby-3.0.0 + [mruby-process](https://github.com/iij/mruby-process) で実行してみるとこんな感じ。

```console
% bin/mruby -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
  PID TT  STAT    TIME COMMAND
93979 23  S+   0:00.01 bin/mruby -e $0 = "進行状況: 永遠の99%"; system("ps #{$$}")
```

Ruby-3.1 preview1 で試すと表示が変更されているのが確認できます。

```console
% ruby31 -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
  PID TT  STAT    TIME COMMAND
94872 23  S+   0:00.15 ruby31: 進行状況: 永遠の99% (ruby31)
```

mruby では `alias` も出来ません。

```console
% bin/mruby -e 'alias $ERROR_INFO $!'
-e:1:17: syntax error, unexpected global variable
```

なのでこのあたりの仕組みを丸ごと追加する必要があります。

ちなみに仮想機械の命令は存在こそしていますが、その先の実装がないため機能することはありません。
もっとも、構文解析で大域変数として処理されるので、命令自体が生成されることもありませぬ。


## やること② エラー情報を保存・取得出来るようにする

すでに示してきましたが CRuby であれば、発生した例外は `rescue` や `ensure` 内部で `$!` を使うと取得できるようになっているし、C 空間からは直接 `rb_errinfo()` で取得可能です。

mruby (3.0.0) では仮想機械のレジスタ上に例外オブジェクトが保存されますが、実行中にこれを取り出せません。
理由はどのレジスタに例外オブジェクトが置かれているかの情報がないためです。
なので取り出せるようにしなければなりません。

`rescue/ensure` ブロックごとに例外オブジェクトを保存するレジスタはコンパイル時に決定されます。
この時の mruby-compiler はレジスタ番号と、`rescue/ensure` ブロックの区間を把握しています。
そのため、このあたりの情報を irep に保存しておくことで解決できそうです。

それでどのように保存させるかについての問題が出てきます。
これについては例外ハンドラを利用できるだろうと考えています。
mruby-3.0.0 では例外・大域ジャンプが起きた場合の例外ハンドラ (`rescue/ensure` ブロック) をコンパイル時に irep へと組み込んでおくようになっているので、これを拡張しましょう。

```
 0 +1: catch handler type
 1 +4: catch handler begin
 5 +4: catch handler end
 9 +4: catch handler target           (rescue/ensureブロックの開始位置)
13 +4: catch handler finish  <<< NEW! (rescue/ensureブロックの終了位置)
17 +2: error registor index  <<< NEW!
19 ..: <END>
```

まあ見て分かるようにハンドラ一つあたり6バイト増えることになります。
RAM どころか ROM も少しでも減らしたい場合、これを削減するために `mrb_irep::flags |= MRB_IREP_NO_ERRINFO` みたいなものを追加してもいいかもしれません。

ここまで見てきたのは Ruby 空間での `rescue/ensure` ブロック内でのことです。
C 空間のことを考えた場合、C での `mrb_rescue()` や `mrb_ensure()` 経由で使うためにこの例外ハンドラテーブルを利用できないことに気がつくでしょう。

ある時、構造体 `mrb_callinfo` を使う方法を思いつきました。`CINFO_ERRINFO` (仮) を導入します。

```c
mrb_callinfo *ci = mrb->c->ci;
ci->cci = CINFO_ERRINFO;        // <<< NEW!
ci->stack[0] = exception_object;
```

`mrb_exc_keep(mrb, exc, userfunc, userdata)` 関数 (仮) を追加して使えるようにすれば良さそうです。
`userfunc()` が呼ばれた内部では、この時に保存された例外オブジェクトが見えるはずです。

副作用は、`mrb_get_args()` 関数は `ci->cci == CINFO_ERRINFO` なフレームを上へ辿るようにする必要が出てくるし、他にも影響が出てくるでしょう。

何はともあれ、これでとりあえずはエラー情報の保存と取得ができそうです。


## やること③ エラー情報を探せるようにする

実際にエラー情報を探索する場合、呼び出し情報 (callinfo; ci) を呼び出し元に辿っていきながら確認することになります。

`ci->cci == CINFO_NONE` あるいは `ci->cci == CINFO_SKIP` であれば、現在実行中の pc が例外ハンドラテーブルの `rescue/ensure` ブロックの区間と一致しているかどうかを見ていきます。
一致すれば示されたレジスタの値が探し求めたエラー情報です。

`ci->cci == CINFO_ERRINFO` であれば、`ci->stack[0]` がエラー情報です。

ci を一番大本まで辿って何もなければエラー情報は `nil` となります。

探索処理は結構重そうですが、そもそも頻繁に使うものではないので許容できると考えています。


## 最後に

これが実現できると、お試しコードで役に立つ `rescue` 修飾子で `p $!` と書けます。

```ruby
内部でエラーが起きるメソッド rescue p $!
```

また、引数なしの `raise` で例外の再送が出来るようになります。

```ruby
begin
  ほげ
rescue
  ふが
  raise
end
```

ただし最初に述べたように、僕は実際の作業を行っていません。
すぐに使いたい人は是非とも挑戦して下さい。
別の方法だとしても歓迎します！ ~~僕もたまに使いたくなるので！~~