Rubyの raise で例外をメッセージ付きで投げ直した時、その例外の new が第一引数を取れなくてもArgumentErrorにならないのはなぜか

tldr; `raise` の 第一引数に渡したオブジェクトの `exception` メソッドの挙動の違いによる。

# `raise` で例外をメッセージ付きで投げ直した時、その例外の `new` が第一引数を取れなくても `ArgumentError` にならない

イニシャライザとして引数を受け取らない `HogeError` 例外を定義する。

```ruby
class HogeError < StandardError
  def initialize
    super('hoge error')
  end
end
```

以下は当然 `#<HogeError: hoge error>` となる

```ruby
#<HogeError: hoge error>
begin
  raise HogeError
rescue => e
  p e
end
```

`HogeError` のイニシャライザは引数を受け取らないので、以下のようにやると `ArgumentError` となる

```ruby
#<ArgumentError: wrong number of arguments (given 1, expected 0)>
begin
  raise HogeError, 'argument error?'
rescue => e
  p e
end
```

だけど、 `rescue` で受け取った `HogeError` のインスタンスをメッセージ付きで `raise` し直すと、 `ArgumentError` にならない。

```ruby
#<HogeError: 投げ直し>
def main
  raise HogeError
rescue => e
  raise e, '投げ直し'
end

begin
  main
rescue => e
  p e
end
```

なぜか。

# ドキュメントを読んでみる

[制御構造 raise - Ruby 3.3 リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#raise) より

> 第一引数で指定された例外を、第二引数をメッセージとして発生させます。

んー？これだとこの挙動が起きる理由がよくわかんないな。

----

[module function Kernel.#fail - Ruby 3.3 リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/raise.html)より

> 引数を渡した場合は、例外メッセージ message を持った error_type の示す例外(省略時 RuntimeError)を発生させます。
>
>error_type として例外ではないクラスやオブジェクトを指定した場合、そのオブジェクトの exception メソッドが返す値を発生する例外にします。その際、exception メソッドに引数として変数 message を渡すことができます。


これもなんか微妙な表現だけど、 `HogeError#exception` が呼ばれるっぽい。 [Exception#exception - Ruby 3.3 リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/Exception.html#I_EXCEPTION)を読んでみよう。

> 引数を指定した場合 自身のコピーを生成し Exception#message 属性を error_message にして返します。
>
> Kernel.#raise は、実質的に、例外オブジェクトの exception メソッドの呼び出しです。

なるほど！ 問題のコードは指定した例外がクラスかインスタンスかの違いがあリ、それがそのまま現れているんだ。 クラスメソッドの `Exception.exception` は `new` のエイリアスだけど、インスタンスメソッドの `Exception#exception` はイニシャライザとは別の動きをして、 `message` 属性を書き換える動きをする。この挙動の違いだった。

```ruby
class HogeError < StandardError
  def initialize
    super('hoge error')
  end
end

raise HogeError, 'これは ArugmentError'
raise HogeError.new, 'これはOK'
```

# 実装を読んでみる

コアの部分の実装を読むときは [TruffleRuby](https://github.com/oracle/truffleruby)([MRIと97%同じ挙動するらしい](https://docs.oracle.com/cd/F44923_01/jdk/20/docs/reference-manual/ruby/#current-status)) の実装を見ると C が読めない私でも結構読めたりするので見てみた。

[Kernel#raise - oracle/truffleruby](https://github.com/oracle/truffleruby/blob/eedda8504b30bd7e023d6d4aba1a49530985cb2e/src/main/ruby/truffleruby/core/kernel.rb#L696) では `Truffle::ExceptionOperations.build_exception_for_raise` を実行して例外を生成している。
```ruby
 def raise(exc = undefined, msg = undefined, ctx = nil, cause: undefined, **kwargs)
    cause_given = !Primitive.undefined?(cause)
    cause = cause_given ? cause : $!   
    # 省略...
    exc = Truffle::ExceptionOperations.build_exception_for_raise(exc, msg)
```

[ExceptionOperations.build_exception_for_raise - oracle/truffleruby](https://github.com/oracle/truffleruby/blob/eedda8504b30bd7e023d6d4aba1a49530985cb2e/src/main/ruby/truffleruby/core/truffle/exception_operations.rb#L20)をみてみると、第一引数 `exc` の `exception` を呼んでいた。

```ruby
    def self.build_exception_for_raise(exc, msg)
      # 省略
      exc = exc.exception msg
```

`Exception.exception` は `new` と同義なので `initialize` を書き換えてると `ArgumentError` になるけど、 `Exception#exception` は `new` とは別の動きをするのでエラーにならない。

```ruby
  class << self
    alias_method :exception, :new
  end

  def exception(message = nil)
    # As strange as this may seem, this is actually the protocol that CRuby implements
    if message and !Primitive.equal?(message, self)
      copy = clone # note: rb_obj_clone() in CRuby
      Primitive.exception_set_message copy, message
      copy
    else
      self
    end
  end
```

[Exception.exception, Exception#exception - oracle/truffleruby](https://github.com/oracle/truffleruby/blob/eedda8504b30bd7e023d6d4aba1a49530985cb2e/src/main/ruby/truffleruby/core/exception.rb#L92-L105)

その通りの実装だ！！